29

背景

从 Android O 开始,应用程序可以具有自适应图标,即 2 层可绘制对象:前景和背景。背景是一个蒙版,可以成为启动器/用户选择的形状,而操作系统也有一个默认形状。

这是 Nova Launcher 允许执行的示例:

在此处输入图像描述

如您所见,它不仅允许选择要使用的形状,还可以完全避免使用形状(在“首选旧图标”中)。

以下是一些关于它的链接:

问题

虽然我知道如何创建一个AdaptiveIconDrawable实例,并且我知道帮助为当前应用程序创建一个的向导,但我不明白在给定 AdaptiveIconDrawable 实例的情况下,启动器如何改变形状。

不仅如此,我记得我看到了一两个允许没有任何形状的发射器。

遗憾的是我找不到关于这部分的任何信息,可能是因为这是一个相对非常新的功能。StackOverflow 上甚至没有关键字。

我试过的

我尝试阅读有关自适应图标的信息,但找不到对接收方的参考。

我知道它里面有2个drawable:

我至少知道如何从第三方应用程序中获取 AdaptiveIconDrawable 实例(假设它有一个):

PackageManager pm = context.getPackageManager();
Intent launchIntentForPackage = pm.getLaunchIntentForPackage(packageName);
String fullPathToActivity = launchIntentForPackage.getComponent().getClassName();
ActivityInfo activityInfo = pm.getActivityInfo(new ComponentName(packageName, fullPathToActivity), 0);
int iconRes = activityInfo.icon;
Drawable drawable = pm.getDrawable(packageName, iconRes, activityInfo.applicationInfo); // will be AdaptiveIconDrawable, if the app has it

问题

  1. 给定一个 AdaptiveIconDrawable 实例,你如何将它塑造成圆形、矩形、圆角矩形、撕裂等等?

  2. 如何删除形状并仍然具有有效的图标大小(使用其前景可绘制)?启动器应用图标的官方尺寸是 48 dp,而 AdaptiveIconDrawable 内部可绘制对象的官方尺寸是 72dp(前景)、108dp(背景)。我想这意味着获取前景可绘制对象,以某种方式调整它的大小,然后转换为位图。

  3. 在哪种情况下使用它到底有用IconCompat.createWithAdaptiveBitmap()?它写道:“如果您正在使用位图构建动态快捷方式,您可能会发现支持库 26.0.0-beta2 的 IconCompat.createWithAdaptiveBitmap() 有助于确保您的位图被正确屏蔽以匹配其他自适应图标。” ,但我不知道它对哪些情况有用。


编辑:为了从自适应图标的前景部分创建位图,同时调整到适当的大小,我认为这可能是一个很好的解决方案:

val foregroundBitmap = convertDrawableToBitmap(drawable.foreground)
val targetSize = convertDpToPixels(this, ...).toInt()
val scaledBitmap = ThumbnailUtils.extractThumbnail(foregroundBitmap, targetSize, targetSize, ThumbnailUtils.OPTIONS_RECYCLE_INPUT)

fun convertDrawableToBitmap(drawable: Drawable?): Bitmap? {
    if (drawable == null)
        return null
    if (drawable is BitmapDrawable) {
        return drawable.bitmap
    }
    val bounds = drawable.bounds
    val width = if (!bounds.isEmpty) bounds.width() else drawable.intrinsicWidth
    val height = if (!bounds.isEmpty) bounds.height() else drawable.intrinsicHeight
    val bitmap = Bitmap.createBitmap(if (width <= 0) 1 else width, if (height <= 0) 1 else height,
            Bitmap.Config.ARGB_8888)
    val canvas = Canvas(bitmap)
    drawable.setBounds(0, 0, canvas.width, canvas.height)
    drawable.draw(canvas)
    drawable.bounds = bounds;
    return bitmap
}

fun convertDpToPixels(context: Context, dp: Float): Float = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.resources.displayMetrics)

可能能够避免同时有 2 个位图,但我认为这没关系。

关于创建各种类型的形状drawable,我仍然不知道该怎么做。我在下面的答案中看到的唯一解决方案是使用圆角矩形或圆形,但还有其他形状(例如撕裂)可以想到。


编辑:谷歌(这里)告诉我应该使用AdaptiveIconDrawable.getIconMask(),但我没有得到任何进一步的信息。但是,我在这里找到了一篇很好的文章。

4

6 回答 6

3

我不明白,给定一个 AdaptiveIconDrawable 实例,启动器如何改变形状。

启动器只是应用程序,因此它们只需按照他们想要的形状(或用户选择的形状)绘制背景,然后在顶部绘制前景。

我没有自己的示例项目,但 Nick Butcher 制作了一个很棒的示例项目和一系列博客文章:AdaptiveIconPlayground


给定一个 AdaptiveIconDrawable 实例,你如何将它塑造成圆形、矩形、圆角矩形、撕裂等等?

最简单的方法是光栅化可绘制对象并使用着色器绘制位图,就像在尼克的AdaptiveIconView中所做的那样:

private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val background: Bitmap

// ...

background = Bitmap.createBitmap(layerSize, layerSize, Bitmap.Config.ARGB_8888)
backgroundPaint.shader = BitmapShader(background, CLAMP, CLAMP)

// < rasterize drawable onto `background` >

// draw desired shape(s)
canvas.drawRoundRect(0f, 0f, iconSize.toFloat(), iconSize.toFloat(),
                cornerRadius, cornerRadius, backgroundPaint)

如何删除形状并仍然具有有效的图标大小(使用其前景可绘制)?启动器应用图标的官方尺寸是 48 dp,而 AdaptiveIconDrawable 内部可绘制对象的官方尺寸是 72dp(前景)、108dp(背景)。我想这意味着获取前景可绘制对象,以某种方式调整它的大小,然后转换为位图。

如果你不想要背景,就不要画它。你完全可以控制。大小并不重要,因为您通常知道应该绘制多大的图标。文档指出前景和背景应为 108dp,因此您可以简单地缩小绘图。如果前景/背景使用矢量图形,那么大小真的无关紧要,因为您可以将它们绘制成您喜欢的大小。

如果您栅格化前景,那么您可以进行自定义绘图,如上所示,或者选择Canvas#drawBitmap(...),它还提供了多个选项来绘制位图,包括传入一个变换矩阵,或者只是一些边界。

如果你不光栅化你的drawable,你也可以使用drawable.setBounds(x1, y1, x2, y2),你可以设置drawable应该在哪里绘制自己的边界。这也应该有效。

在哪种情况下,使用 IconCompat.createWithAdaptiveBitmap() 到底有用吗?它写道:“如果您正在使用位图构建动态快捷方式,您可能会发现支持库 26.0.0-beta2 的 IconCompat.createWithAdaptiveBitmap() 有助于确保您的位图被正确屏蔽以匹配其他自适应图标。” ,但我不知道它对哪些情况有用。

ShortCutInfo.Builder有一个setIcon(Icon icon)你需要传入的方法。(同样适用于兼容版本)

似乎 Icon 用于控制作为图标传入的位图类型。现在我找不到 Icon 的任何其他用途。我认为您在创建启动器时不会使用它。


反映最后评论的更多信息

您是否使用自己的可绘制对象包装 AdaptiveIconDrawable 类?我只是想以某种方式将它转换为我可以使用的东西,ImageView 和 Bitmap,我希望使用上面屏幕截图中显示的所有形状来控制形状。我该怎么做?

如果您按照上面的链接,您可以看到AdaptiveIconView绘制 AdaptiveIconDrawable 的自定义,因此自定义视图绝对是一种选择,但提到的所有内容都可以轻松移动到自定义 Drawable 中,然后您也可以将其与基本 ImageView 一起使用.

您可以使用可用的方法Canvas以及BitmapShader如上所示的方法来实现各种不同的背景,例如,除了drawRoundRect我们将拥有

canvas.drawCircle(centerX, centerY, radius, backgroundPaint) // circle
canvas.drawRect(0f, 0f, width, height, backgroundPaint) // rect
canvas.drawPath(path, backgroundPaint) // more complex shapes

要在背景形状之间切换,您可以使用从 if/else、过度合成到继承的任何方法,然后绘制您喜欢的形状。

于 2017-12-19T18:59:28.083 回答
1

好的,我有一些工作要做,但由于某种原因,内部图标似乎比使用 AdaptiveIconDrawable 所做的要小。同样出于某种原因,在途中,它影响了原始的 AdaptiveIconDrawable(即使我在我使用mutate的任何可绘制对象上使用过),所以我必须创建一个新的来展示原始与新的。另一个小烦恼是,要创建蒙版位图,我必须有 2 个 Bitmap 实例(drawable 转换为一个,并且也需要一个输出)。

我想知道是否可以将可绘制对象直接转换为具有给定形状的位图/可绘制对象,所以我在这里询问了这个问题。

所以,假设你有一个Path实例。您可以从AdaptiveIconDrawable.getIconMask函数中获取一个(这是系统的一个),或者您可以自己创建一个,例如这里使用的一个 repository here)或here

如果有人知道如何解决我上面提到的那些问题(较小的前景并影响原始可绘制对象,也许还有更好的转换),请告诉我。目前,您可以使用此解决方案,也可以使用此处的库。

现在,假设您获得了 AdaptiveIconDrawable 实例,并且您希望将其塑造成与该Path实例相同的形状。

因此,您可以做的类似于下面的内容(PathUtils 从任一存储库转换为 Kotlin),结果如下:

在此处输入图像描述

MainActivity.kt

class MainActivity : AppCompatActivity(R.layout.activity_main) {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val appIcon = applicationInfo.loadIcon(packageManager)
        originalIconImageView.setImageDrawable(applicationInfo.loadIcon(packageManager))
        if (appIcon is AdaptiveIconDrawable) {
            val iconMask = getPath(PATH_SQUIRCLE)
            val maskedBitmap = getMaskedBitmap(appIcon.background, iconMask)
            val foreground = appIcon.foreground
            val layerDrawable = LayerDrawable(arrayOf(BitmapDrawable(resources, maskedBitmap), foreground))
            maskedImageView.setImageDrawable(layerDrawable)
        }
    }

    companion object {
        const val PATH_CIRCLE = 0
        const val PATH_SQUIRCLE = 1
        const val PATH_ROUNDED_SQUARE = 2
        const val PATH_SQUARE = 3
        const val PATH_TEARDROP = 4

        fun resizePath(path: Path, width: Float, height: Float): Path {
            val bounds = RectF(0f, 0f, width, height)
            val resizedPath = Path(path)
            val src = RectF()
            resizedPath.computeBounds(src, true)
            val resizeMatrix = Matrix()
            resizeMatrix.setRectToRect(src, bounds, Matrix.ScaleToFit.CENTER)
            resizedPath.transform(resizeMatrix)
            return resizedPath
        }

        fun getMaskedBitmap(src: Bitmap, path: Path, resizePathToMatchBitmap: Boolean = true): Bitmap {
            val pathToUse = if (resizePathToMatchBitmap) resizePath(path, src.width.toFloat(), src.height.toFloat()) else path
            val output = Bitmap.createBitmap(src.width, src.height, Bitmap.Config.ARGB_8888)
            val canvas = Canvas(output)
            val paint = Paint(Paint.ANTI_ALIAS_FLAG)
            paint.color = 0XFF000000.toInt()
            canvas.drawPath(pathToUse, paint)
            paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
            canvas.drawBitmap(src, 0f, 0f, paint)
            return output
        }

        fun getMaskedBitmap(drawable: Drawable, path: Path, resizePathToMatchBitmap: Boolean = true): Bitmap = getMaskedBitmap(drawable.toBitmap(), path, resizePathToMatchBitmap)

        fun getPath(pathType: Int): Path {
            val path = Path()
            val pathSize = Rect(0, 0, 50, 50)
            when (pathType) {
                PATH_CIRCLE -> {
                    path.arcTo(RectF(pathSize), 0f, 359f)
                    path.close()
                }
                PATH_SQUIRCLE -> path.set(PathUtils.createPathFromPathData("M 50,0 C 10,0 0,10 0,50 C 0,90 10,100 50,100 C 90,100 100,90 100,50 C 100,10 90,0 50,0 Z"))
                PATH_ROUNDED_SQUARE -> path.set(PathUtils.createPathFromPathData("M 50,0 L 70,0 A 30,30,0,0 1 100,30 L 100,70 A 30,30,0,0 1 70,100 L 30,100 A 30,30,0,0 1 0,70 L 0,30 A 30,30,0,0 1 30,0 z"))
                PATH_SQUARE -> {
                    path.lineTo(0f, 50f)
                    path.lineTo(50f, 50f)
                    path.lineTo(50f, 0f)
                    path.lineTo(0f, 0f)
                    path.close()
                }
                PATH_TEARDROP -> path.set(PathUtils.createPathFromPathData("M 50,0 A 50,50,0,0 1 100,50 L 100,85 A 15,15,0,0 1 85,100 L 50,100 A 50,50,0,0 1 50,0 z"))
            }
            return path
        }

    }
}

activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Original:" />

    <ImageView
        android:id="@+id/originalIconImageView"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_marginTop="16dp" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="Masked:" />

    <ImageView
        android:id="@+id/maskedImageView"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_marginTop="16dp" />
</LinearLayout>

PathUtils.kt

object PathUtils {
    /**
     * @param pathData The string representing a path, the same as "d" string in svg file.
     * @return the generated Path object.
     */
    fun createPathFromPathData(pathData: String): Path {
        val path = Path()
        val nodes = createNodesFromPathData(pathData)
        PathDataNode.nodesToPath(nodes, path)
        return path
    }

    /**
     * @param pathData The string representing a path, the same as "d" string in svg file.
     * @return an array of the PathDataNode.
     */
    fun createNodesFromPathData(pathData: String): Array<PathDataNode> {
        var start = 0
        var end = 1
        val list = ArrayList<PathDataNode>()
        while (end < pathData.length) {
            end = nextStart(pathData, end)
            val s = pathData.substring(start, end)
            val `val` = getFloats(s)
            addNode(list, s[0], `val`)
            start = end
            end++
        }
        if (end - start == 1 && start < pathData.length) {
            addNode(list, pathData[start], FloatArray(0))
        }
        return list.toTypedArray()
    }

    private fun nextStart(s: String, inputEnd: Int): Int {
        var end = inputEnd
        var c: Char
        while (end < s.length) {
            c = s[end]
            if ((c - 'A') * (c - 'Z') <= 0 || (c - 'a') * (c - 'z') <= 0) return end
            end++
        }
        return end
    }

    private fun addNode(list: ArrayList<PathDataNode>, cmd: Char, `val`: FloatArray) {
        list.add(PathDataNode(cmd, `val`))
    }

    /**
     * Parse the floats in the string.
     * This is an optimized version of parseFloat(s.split(",|\\s"));
     *
     * @param s the string containing a command and list of floats
     * @return array of floats
     */
    @Throws(NumberFormatException::class)
    private fun getFloats(s: String): FloatArray {
        if (s[0] == 'z' || s[0] == 'Z')
            return FloatArray(0)
        val tmp = FloatArray(s.length)
        var count = 0
        var pos = 1
        var end: Int
        while (extract(s, pos).also { end = it } >= 0) {
            if (pos < end) tmp[count++] = s.substring(pos, end).toFloat()
            pos = end + 1
        }
        // handle the final float if there is one
        if (pos < s.length) tmp[count++] = s.substring(pos).toFloat()
        return tmp.copyOf(count)
    }

    /**
     * Calculate the position of the next comma or space
     *
     * @param s     the string to search
     * @param start the position to start searching
     * @return the position of the next comma or space or -1 if none found
     */
    private fun extract(s: String, start: Int): Int {
        val space = s.indexOf(' ', start)
        val comma = s.indexOf(',', start)
        if (space == -1) return comma
        return if (comma == -1) space else Math.min(comma, space)
    }

    class PathDataNode(private val type: Char, private var params: FloatArray) {
        @Suppress("unused")
        constructor(n: PathDataNode) : this(n.type, n.params.copyOf(n.params.size))

        companion object {
            fun nodesToPath(node: Array<PathDataNode>, path: Path) {
                val current = FloatArray(4)
                var previousCommand = 'm'
                for (pathDataNode in node) {
                    addCommand(path, current, previousCommand, pathDataNode.type, pathDataNode.params)
                    previousCommand = pathDataNode.type
                }
            }

            private fun addCommand(path: Path, current: FloatArray, inputPreviousCmd: Char, cmd: Char, floats: FloatArray) {
                var previousCmd = inputPreviousCmd
                var incr = 2
                var currentX = current[0]
                var currentY = current[1]
                var ctrlPointX = current[2]
                var ctrlPointY = current[3]
                var reflectiveCtrlPointX: Float
                var reflectiveCtrlPointY: Float
                when (cmd) {
                    'z', 'Z' -> {
                        path.close()
                        return
                    }
                    'm', 'M', 'l', 'L', 't', 'T' -> incr = 2
                    'h', 'H', 'v', 'V' -> incr = 1
                    'c', 'C' -> incr = 6
                    's', 'S', 'q', 'Q' -> incr = 4
                    'a', 'A' -> incr = 7
                }
                var k = 0
                while (k < floats.size) {
                    when (cmd) {
                        'm' -> {
                            path.rMoveTo(floats[k], floats[k + 1])
                            currentX += floats[k]
                            currentY += floats[k + 1]
                        }
                        'M' -> {
                            path.moveTo(floats[k], floats[k + 1])
                            currentX = floats[k]
                            currentY = floats[k + 1]
                        }
                        'l' -> {
                            path.rLineTo(floats[k], floats[k + 1])
                            currentX += floats[k]
                            currentY += floats[k + 1]
                        }
                        'L' -> {
                            path.lineTo(floats[k], floats[k + 1])
                            currentX = floats[k]
                            currentY = floats[k + 1]
                        }
                        'h' -> {
                            path.rLineTo(floats[k], 0f)
                            currentX += floats[k]
                        }
                        'H' -> {
                            path.lineTo(floats[k], currentY)
                            currentX = floats[k]
                        }
                        'v' -> {
                            path.rLineTo(0f, floats[k])
                            currentY += floats[k]
                        }
                        'V' -> {
                            path.lineTo(currentX, floats[k])
                            currentY = floats[k]
                        }
                        'c' -> {
                            path.rCubicTo(floats[k], floats[k + 1], floats[k + 2], floats[k + 3], floats[k + 4], floats[k + 5])
                            ctrlPointX = currentX + floats[k + 2]
                            ctrlPointY = currentY + floats[k + 3]
                            currentX += floats[k + 4]
                            currentY += floats[k + 5]
                        }
                        'C' -> {
                            path.cubicTo(floats[k], floats[k + 1], floats[k + 2], floats[k + 3],
                                    floats[k + 4], floats[k + 5])
                            currentX = floats[k + 4]
                            currentY = floats[k + 5]
                            ctrlPointX = floats[k + 2]
                            ctrlPointY = floats[k + 3]
                        }
                        's' -> {
                            reflectiveCtrlPointX = 0f
                            reflectiveCtrlPointY = 0f
                            if (previousCmd == 'c' || previousCmd == 's' || previousCmd == 'C' || previousCmd == 'S') {
                                reflectiveCtrlPointX = currentX - ctrlPointX
                                reflectiveCtrlPointY = currentY - ctrlPointY
                            }
                            path.rCubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY, floats[k], floats[k + 1], floats[k + 2], floats[k + 3])
                            ctrlPointX = currentX + floats[k]
                            ctrlPointY = currentY + floats[k + 1]
                            currentX += floats[k + 2]
                            currentY += floats[k + 3]
                        }
                        'S' -> {
                            reflectiveCtrlPointX = currentX
                            reflectiveCtrlPointY = currentY
                            if (previousCmd == 'c' || previousCmd == 's' || previousCmd == 'C' || previousCmd == 'S') {
                                reflectiveCtrlPointX = 2 * currentX - ctrlPointX
                                reflectiveCtrlPointY = 2 * currentY - ctrlPointY
                            }
                            path.cubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY, floats[k], floats[k + 1], floats[k + 2], floats[k + 3])
                            ctrlPointX = floats[k]
                            ctrlPointY = floats[k + 1]
                            currentX = floats[k + 2]
                            currentY = floats[k + 3]
                        }
                        'q' -> {
                            path.rQuadTo(floats[k], floats[k + 1], floats[k + 2], floats[k + 3])
                            ctrlPointX = currentX + floats[k]
                            ctrlPointY = currentY + floats[k + 1]
                            currentX += floats[k + 2]
                            currentY += floats[k + 3]
                        }
                        'Q' -> {
                            path.quadTo(floats[k], floats[k + 1], floats[k + 2], floats[k + 3])
                            ctrlPointX = floats[k]
                            ctrlPointY = floats[k + 1]
                            currentX = floats[k + 2]
                            currentY = floats[k + 3]
                        }
                        't' -> {
                            reflectiveCtrlPointX = 0f
                            reflectiveCtrlPointY = 0f
                            if (previousCmd == 'q' || previousCmd == 't' || previousCmd == 'Q' || previousCmd == 'T') {
                                reflectiveCtrlPointX = currentX - ctrlPointX
                                reflectiveCtrlPointY = currentY - ctrlPointY
                            }
                            path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
                                    floats[k], floats[k + 1])
                            ctrlPointX = currentX + reflectiveCtrlPointX
                            ctrlPointY = currentY + reflectiveCtrlPointY
                            currentX += floats[k]
                            currentY += floats[k + 1]
                        }
                        'T' -> {
                            reflectiveCtrlPointX = currentX
                            reflectiveCtrlPointY = currentY
                            if (previousCmd == 'q' || previousCmd == 't' || previousCmd == 'Q' || previousCmd == 'T') {
                                reflectiveCtrlPointX = 2 * currentX - ctrlPointX
                                reflectiveCtrlPointY = 2 * currentY - ctrlPointY
                            }
                            path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, floats[k], floats[k + 1])
                            ctrlPointX = reflectiveCtrlPointX
                            ctrlPointY = reflectiveCtrlPointY
                            currentX = floats[k]
                            currentY = floats[k + 1]
                        }
                        'a' -> {
                            // (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
                            drawArc(path, currentX, currentY, floats[k + 5] + currentX, floats[k + 6] + currentY, floats[k],
                                    floats[k + 1], floats[k + 2], floats[k + 3] != 0f, floats[k + 4] != 0f)
                            currentX += floats[k + 5]
                            currentY += floats[k + 6]
                            ctrlPointX = currentX
                            ctrlPointY = currentY
                        }
                        'A' -> {
                            drawArc(path, currentX, currentY, floats[k + 5], floats[k + 6], floats[k], floats[k + 1], floats[k + 2],
                                    floats[k + 3] != 0f, floats[k + 4] != 0f)
                            currentX = floats[k + 5]
                            currentY = floats[k + 6]
                            ctrlPointX = currentX
                            ctrlPointY = currentY
                        }
                    }
                    previousCmd = cmd
                    k += incr
                }
                current[0] = currentX
                current[1] = currentY
                current[2] = ctrlPointX
                current[3] = ctrlPointY
            }

            private fun drawArc(p: Path, x0: Float, y0: Float, x1: Float, y1: Float, a: Float, b: Float, theta: Float, isMoreThanHalf: Boolean, isPositiveArc: Boolean) {
                /* Convert rotation angle from degrees to radians */
                val thetaD = Math.toRadians(theta.toDouble())
                /* Pre-compute rotation matrix entries */
                val cosTheta = Math.cos(thetaD)
                val sinTheta = Math.sin(thetaD)
                /* Transform (x0, y0) and (x1, y1) into unit space */
                /* using (inverse) rotation, followed by (inverse) scale */
                val x0p = (x0 * cosTheta + y0 * sinTheta) / a
                val y0p = (-x0 * sinTheta + y0 * cosTheta) / b
                val x1p = (x1 * cosTheta + y1 * sinTheta) / a
                val y1p = (-x1 * sinTheta + y1 * cosTheta) / b
                /* Compute differences and averages */
                val dx = x0p - x1p
                val dy = y0p - y1p
                val xm = (x0p + x1p) / 2
                val ym = (y0p + y1p) / 2
                /* Solve for intersecting unit circles */
                val dsq = dx * dx + dy * dy
                if (dsq == 0.0) return  /* Points are coincident */
                val disc = 1.0 / dsq - 1.0 / 4.0
                if (disc < 0.0) {
                    val adjust = (Math.sqrt(dsq) / 1.99999).toFloat()
                    drawArc(p, x0, y0, x1, y1, a * adjust, b * adjust, theta, isMoreThanHalf, isPositiveArc)
                    return  /* Points are too far apart */
                }
                val s = Math.sqrt(disc)
                val sdx = s * dx
                val sdy = s * dy
                var cx: Double
                var cy: Double
                if (isMoreThanHalf == isPositiveArc) {
                    cx = xm - sdy
                    cy = ym + sdx
                } else {
                    cx = xm + sdy
                    cy = ym - sdx
                }
                val eta0 = Math.atan2(y0p - cy, x0p - cx)
                val eta1 = Math.atan2(y1p - cy, x1p - cx)
                var sweep = eta1 - eta0
                if (isPositiveArc != sweep >= 0) {
                    if (sweep > 0) {
                        sweep -= 2 * Math.PI
                    } else {
                        sweep += 2 * Math.PI
                    }
                }
                cx *= a.toDouble()
                cy *= b.toDouble()
                val tcx = cx
                cx = cx * cosTheta - cy * sinTheta
                cy = tcx * sinTheta + cy * cosTheta
                arcToBezier(p, cx, cy, a.toDouble(), b.toDouble(), x0.toDouble(), y0.toDouble(), thetaD, eta0, sweep)
            }

            /**
             * Converts an arc to cubic Bezier segments and records them in p.
             *
             * @param p     The target for the cubic Bezier segments
             * @param cx    The x coordinate center of the ellipse
             * @param cy    The y coordinate center of the ellipse
             * @param a     The radius of the ellipse in the horizontal direction
             * @param b     The radius of the ellipse in the vertical direction
             * @param inputE1x   E(eta1) x coordinate of the starting point of the arc
             * @param inputE1y   E(eta2) y coordinate of the starting point of the arc
             * @param theta The angle that the ellipse bounding rectangle makes with horizontal plane
             * @param start The start angle of the arc on the ellipse
             * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse
             */
            private fun arcToBezier(p: Path, cx: Double, cy: Double, a: Double, b: Double, inputE1x: Double, inputE1y: Double, theta: Double, start: Double, sweep: Double) {
                // Taken from equations at: http://spaceroots.org/documents/ellipse/node8.html
                // and http://www.spaceroots.org/documents/ellipse/node22.html
                // Maximum of 45 degrees per cubic Bezier segment
                var e1x = inputE1x
                var e1y = inputE1y
                val numSegments = Math.abs(Math.ceil(sweep * 4 / Math.PI).toInt())
                var eta1 = start
                val cosTheta = Math.cos(theta)
                val sinTheta = Math.sin(theta)
                val cosEta1 = Math.cos(eta1)
                val sinEta1 = Math.sin(eta1)
                var ep1x = -a * cosTheta * sinEta1 - b * sinTheta * cosEta1
                var ep1y = -a * sinTheta * sinEta1 + b * cosTheta * cosEta1
                val anglePerSegment = sweep / numSegments
                for (i in 0 until numSegments) {
                    val eta2 = eta1 + anglePerSegment
                    val sinEta2 = Math.sin(eta2)
                    val cosEta2 = Math.cos(eta2)
                    val e2x = cx + a * cosTheta * cosEta2 - b * sinTheta * sinEta2
                    val e2y = cy + a * sinTheta * cosEta2 + b * cosTheta * sinEta2
                    val ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2
                    val ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2
                    val tanDiff2 = Math.tan((eta2 - eta1) / 2)
                    val alpha = Math.sin(eta2 - eta1) * (Math.sqrt(4 + 3 * tanDiff2 * tanDiff2) - 1) / 3
                    val q1x = e1x + alpha * ep1x
                    val q1y = e1y + alpha * ep1y
                    val q2x = e2x - alpha * ep2x
                    val q2y = e2y - alpha * ep2y
                    p.cubicTo(q1x.toFloat(), q1y.toFloat(), q2x.toFloat(), q2y.toFloat(), e2x.toFloat(), e2y.toFloat())
                    eta1 = eta2
                    e1x = e2x
                    e1y = e2y
                    ep1x = ep2x
                    ep1y = ep2y
                }
            }
        }
    }
}

于 2020-05-01T10:53:14.563 回答
0

我做了一个自定义ImageView,可以设置一个路径来剪辑背景/可绘制并通过自定义轮廓提供程序应用适当的阴影,其中包括对读取系统首选项的支持(如在我的 Pixel 4/emulators 上确认的那样,更改系统图标形状是传播到我的应用程序。)

看法:

import android.content.Context
import android.graphics.*
import android.graphics.drawable.Drawable
import android.os.Build
import android.util.AttributeSet
import android.view.View
import android.view.ViewOutlineProvider
import androidx.appcompat.widget.AppCompatImageView

open class AdaptiveImageView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {

    // Reusable to reduce object allocation
    private val resizeRect = RectF()
    private val srcResizeRect = RectF()
    private val resizeMatrix = Matrix()

    private val adaptivePathPreference = Path()
    private val adaptivePathResized = Path()

    private var backgroundDelegate: Drawable? = null

    // Paint to clear area outside adaptive path
    private val clearPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
    }

    init {
        // Use the adaptive path as an outline provider
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            outlineProvider = object : ViewOutlineProvider() {
                override fun getOutline(view: View, outline: Outline) {
                    outline.setConvexPath(adaptivePathResized)
                }
            }
        }
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        updatePathBounds()
    }

    // We use saveLayer/clear rather than clipPath so we get anti-aliasing
    override fun onDraw(canvas: Canvas) {
        val count = canvas.saveLayer(0f, 0f, width.toFloat(), height.toFloat(), null)
        backgroundDelegate?.draw(canvas)
        super.onDraw(canvas)
        canvas.drawPath(adaptivePathResized, clearPaint)
        canvas.restoreToCount(count)
    }

    // Background doesn't play nice with our clipping, so hold drawable and null out so
    // we can handle ourselves later.
    override fun setBackground(background: Drawable?) {
        backgroundDelegate = background?.apply {
            if (isStateful) state = drawableState
        }

        if (isLaidOut) updatePathBounds()

        // Null out so noone else tries to draw it (incorrectly)
        super.setBackground(null)
    }

    override fun drawableStateChanged() {
        super.drawableStateChanged()
        backgroundDelegate?.apply {
            if (isStateful) state = drawableState
        }
    }

    fun setAdaptivePath(path: Path?) {
        path?.let { adaptivePathPreference.set(it) } ?: adaptivePathPreference.reset()
        updatePathBounds()
    }

    private fun updatePathBounds() {
        resizePath(
            left = paddingLeft.toFloat(),
            top = paddingTop.toFloat(),
            right = width - paddingRight.toFloat(),
            bottom = height - paddingBottom.toFloat()
        )

        backgroundDelegate?.apply {
            setBounds(
                paddingLeft,
                paddingTop,
                width,
                height
            )
        }

        invalidate()
        invalidateOutline()
    }

    // No object allocations
    private fun resizePath(left: Float, top: Float, right: Float, bottom: Float) {
        resizeRect.set(left, top, right, bottom)
        adaptivePathResized.set(adaptivePathPreference)
        srcResizeRect.set(0f, 0f, 0f, 0f)
        adaptivePathResized.computeBounds(srcResizeRect, true)
        resizeMatrix.reset()
        resizeMatrix.setRectToRect(srcResizeRect, resizeRect, Matrix.ScaleToFit.CENTER)
        adaptivePathResized.transform(resizeMatrix)

        // We want to invert the path so we can clear it later
        adaptivePathResized.fillType = Path.FillType.INVERSE_EVEN_ODD
    }
}

路径枚举/函数:


private val circlePath = Path().apply {
    arcTo(RectF(0f, 0f, 50f, 50f), 0f, 359f)
    close()
}

private val squirclePath = Path().apply { set(PathParser.createPathFromPathData("M 50,0 C 10,0 0,10 0,50 C 0,90 10,100 50,100 C 90,100 100,90 100,50 C 100,10 90,0 50,0 Z")) }

private val roundedPath = Path().apply { set(PathParser.createPathFromPathData("M 50,0 L 70,0 A 30,30,0,0 1 100,30 L 100,70 A 30,30,0,0 1 70,100 L 30,100 A 30,30,0,0 1 0,70 L 0,30 A 30,30,0,0 1 30,0 z")) }

private val squarePath = Path().apply {
    lineTo(0f, 50f)
    lineTo(50f, 50f)
    lineTo(50f, 0f)
    lineTo(0f, 0f)
    close()
}

private val tearDropPath = Path().apply { set(PathParser.createPathFromPathData("M 50,0 A 50,50,0,0 1 100,50 L 100,85 A 15,15,0,0 1 85,100 L 50,100 A 50,50,0,0 1 50,0 z")) }

private val shieldPath = Path().apply { set(PathParser.createPathFromPathData("m6.6146,13.2292a6.6146,6.6146 0,0 0,6.6146 -6.6146v-5.3645c0,-0.6925 -0.5576,-1.25 -1.2501,-1.25L6.6146,-0 1.2501,-0C0.5576,0 0,0.5575 0,1.25v5.3645A6.6146,6.6146 0,0 0,6.6146 13.2292Z")) }

private val lemonPath = Path().apply { set(PathParser.createPathFromPathData("M1.2501,0C0.5576,0 0,0.5576 0,1.2501L0,6.6146A6.6146,6.6146 135,0 0,6.6146 13.2292L11.9791,13.2292C12.6716,13.2292 13.2292,12.6716 13.2292,11.9791L13.2292,6.6146A6.6146,6.6146 45,0 0,6.6146 0L1.2501,0z")) }

enum class IconPath(val path: () -> Path?) {
    SYSTEM(
        path = {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                val adaptive = AdaptiveIconDrawable(null, null)
                adaptive.iconMask
            } else {
                null
            }
        }
    ),
    CIRCLE(path = { circlePath }),
    SQUIRCLE(path = { squirclePath }),
    ROUNDED(path = { roundedPath }),
    SQUARE(path = { squarePath }),
    TEARDROP(path = { tearDropPath }),
    SHIELD(path = { shieldPath }),
    LEMON(path = { lemonPath });
}

复制系统偏好的关键是创建一个空AdaptiveIconDrawable并读出图标掩码(我们稍后调整其大小以在视图中使用。这将始终返回当前系统图标形状路径。

示例用法:

myAdapativeImageView.setAdaptivePath(IconPath.SYSTEM.path())

例子:

在此处输入图像描述

于 2020-05-08T02:14:11.363 回答
0

启动器的限制比应用程序少得多,因此它们可以使用其他方法,但在 Nick Butcher 的Adaptive Icon Playground中很好地展示了一种解决方案。

您可能感兴趣的类是自适应图标视图,它通过创建每个图层的栅格,将背景作为画布位图,然后将这些图层绘制为圆角矩形来实现剪辑,从而呈现图标的改编版本。

链接的 repo 将提供更多信息,并包括如何转换图层以获得运动效果等的示例,但这里是在图像视图中“调整图标”的基本伪代码:

setIcon() {
    //erase canvas first...
    canvas.setBitmap(background)
    drawable.setBounds(0, 0, layerSize, layerSize)
    drawable.draw(canvas)
}

onDraw() {
    //draw shadow first if needed...
    canvas.drawRoundRect(..., cornerRadius, backgroundPaint)
    canvas.drawRoundRect(..., cornerRadius, foregroundPaint)
}
于 2017-12-22T12:43:28.807 回答
0

我知道从 AdaptiveIconDrawable 构建自定义形状图标的两种方法。然而,我认为谷歌应该制定一个公共AdaptiveIconDrawable.setMask(Path path)方法:

第一种方式(与AOSP 代码完全相同):

public Bitmap createBitmap(@NonNull AdaptiveIconDrawable drawable, @NonNull Path path, int outputSize) {

    // make the drawable match the output size and store its bounds to restore later
    final Rect originalBounds = drawable.getBounds();
    drawable.setBounds(0, 0, outputSize, outputSize);

    // rasterize drawable
    final Bitmap outputBitmap = Bitmap.createBitmap(outputSize, outputSize, Bitmap.Config.ARGB_8888);
    final Canvas tmpCanvas = new Canvas(maskBitmap);
    drawable.getBackground().draw(tmpCanvas);
    drawable.getForeground().draw(tmpCanvas);

    // build a paint with shader composed by the rasterized AdaptiveIconDrawable
    final BitmapShader shader = new BitmapShader(outputBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG |
            Paint.FILTER_BITMAP_FLAG);
    paint.setShader(shader);

    // draw the shader with custom path (shape)
    tmpCanvas.drawPath(path, paint);

    // restore drawable original bounds
    drawable.setBounds(originalBounds);

    return outputBitmap;

}

第二种方式(我最喜欢的一种,因为它允许在需要多次使用的情况下缓存遮罩位图,避免位图、画布、位图着色器和油漆重新分配)。如果您不明白,请确保查看此链接

@Nullable private Bitmap mMaskBitmap;
@Nullable private Paint mClearPaint;

@NonNull Canvas mCanvas = new Canvas();

@Nullable Path mCustomShape; // your choice

@Nullable Rect mOldBounds;

public Bitmap createBitmap(@NonNull AdaptiveIconDrawable drawable, int outputSize) {
    final Bitmap outputBitmap = Bitmap.createBitmap(outputSize, outputSize, Bitmap.Config.ARGB_8888);
    mCanvas.setBitmap(outputBitmap);

    // rasterize the AdaptiveIconDrawable
    mOldBounds = drawable.getBounds();
    drawable.setBounds(0, 0, outputSize, outputSize);
    drawable.getBackground().draw(mCanvas);
    drawable.getForeground().draw(mCanvas);

    // finally mask the bitmap, generating the desired output shape
    // this clears all the pixels of the rasterized AdaptiveIconDrawable which
    // fall below the maskBitmap BLACK pixels
    final Bitmap maskBitmap = getMaskBitmap(mCustomShape, outputSize);
    mCanvas.drawBitmap(maskBitmap, 0, 0, mClearPaint);

    // restore original drawable bounds
    drawable.setBounds(mOldBounds);

    return outputBitmap;
}

// results a bitmap with the mask of the @path shape
private Bitmap getMaskBitmap(@Nullable Path path, int iconSize) {
    if (mMaskBitmap != null && mMaskBitmap.getWidth() == iconSize && mMaskBitmap.getHeight() == iconSize)
        // quick return if already cached AND size-compatible
        return mMaskBitmap;

    // just create a plain, black bitmap with the same size of AdaptiveIconDrawable
    mMaskBitmap = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ALPHA_8);
    mMaskBitmap.eraseColor(Color.BLACK);
    final Canvas tmpCanvas = new Canvas(mMaskBitmap);

    // clear the pixels inside the shape (those where the icon will be visible)
    mClearPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
    mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
    if (path != null) 
        // if path is null, the output adaptive icon will not be masked (square, full size)
        tmpCanvas.drawPath(path, mClearPaint);

    return mMaskBitmap;
}

我更喜欢第二种方式,但最好的方式取决于使用情况。如果只塑造了一个图标,那么第一个图标就可以完成这项工作。但是,对于多个图标,第二个更好。分享你的意见

于 2020-08-23T23:32:51.197 回答
-1

由于 Launcher 只是一个 Activity,因此您可以绘制任何东西。您可以绘制应用程序图标,例如在美丽的动画云上运行的小马。这是你的世界,它只遵守你的规则。

进一步......编程世界没有魔法。如果您遇到魔法,只需使用反编译器(使用 Java 很容易),找到负责魔法的代码,记录它并写一篇关于这种魔法如何工作的精彩博客文章。

给定一个 AdaptiveIconDrawable 实例,你如何将它塑造成圆形、矩形、圆角矩形、撕裂等等?

您可以使用 AdaptiveIconDrawable.getBackground() 并向其添加任何蒙版。实际上,你可以用图标做任何你想做的事情,AdaptiveIconDrawable 只是一种方式,你可以轻松地分割前景和背景,无需复杂的过滤器或神经网络。添加视差、动画和更多效果,现在你有 2 层。

于 2017-12-12T03:47:29.683 回答