Как средства запуска изменяют форму адаптивного значка, в том числе удаляют фон?

Фон

Начиная с Android O, приложения могут иметь адаптивные значки, которые представляют собой 2 слоя рисования: передний план и фон. Фон - это маска, которая становится формой по выбору программы запуска / пользователя, в то время как ОС тоже имеет форму по умолчанию.

Вот пример того, что Nova Launcher позволяет делать:

Как вы можете видеть, он позволяет не только выбирать, какую фигуру использовать, но и вообще избегать фигуры (в разделе "предпочитать устаревшие значки").

Вот несколько ссылок об этом:

Эта проблема

Хотя я знаю, как создать экземпляр AdaptiveIconDrawable, и мне известен мастер, помогающий создать его для текущего приложения, я не понимаю, как, учитывая экземпляр AdaptiveIconDrawable, средства запуска изменяют форму.

Мало того, но я помню, что видел одну или две пусковые установки, позволяющие не иметь никакой формы.

К сожалению, я не могу найти какую-либо информацию об этой части, возможно, потому что это относительно очень новая функция. Здесь даже нет ключевого слова для этого в Stackru.

Что я пробовал

Я попытался прочитать об адаптивных значках, но не смог найти ссылку на приемник.

Я знаю, что в нем есть 2 drawables:

Я знаю, по крайней мере, как получить экземпляр 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, в то время как официальные размеры для внутренних Drawables AdaptiveIconDrawable - 72dp (на переднем плане), 108dp (на заднем плане). Я предполагаю, что это будет означать создание переднего плана для рисования, изменение его размера и преобразование в растровое изображение.

  3. В каком случае именно это полезно использовать IconCompat.createWithAdaptiveBitmap()? Было написано, что "если вы создаете динамический ярлык с использованием растрового изображения, вы можете найти в IconCompat.createWithAdaptiveBitmap() библиотеки поддержки 26.0.0-beta2 полезную гарантию того, что растровое изображение правильно маскируется для соответствия другим адаптивным значкам"., но я не понимаю, в каких случаях это полезно.


РЕДАКТИРОВАТЬ: Чтобы создать растровое изображение из переднего плана части адаптивного значка, при изменении размера до нужного размера, я думаю, что это может быть хорошим решением:

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)

Может быть, удастся избежать двух битовых карт одновременно, но это нормально, я думаю.

Что касается создания фигурного рисунка различных типов, я до сих пор не знаю, как это сделать. Единственное решение, которое я видел в ответах ниже, - это использование скругленного прямоугольника или круга, но есть и другие формы (например, слеза), которые могут прийти в голову.

7 ответов

Я не понимаю, как, учитывая экземпляр AdaptiveIconDrawable, средства запуска изменяют форму.

Средства запуска - это просто приложения, поэтому они просто рисуют фон в нужной форме (или выбранном пользователем), а затем рисуют передний план сверху.

У меня нет собственного примера проекта, но Ник Бутчер сделал отличный пример проекта и серию постов в блоге: https://github.com/nickbutcher/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, в то время как официальные размеры для внутренних Drawables AdaptiveIconDrawable - 72dp (на переднем плане), 108dp (на заднем плане). Я предполагаю, что это будет означать создание переднего плана для рисования, изменение его размера и преобразование в растровое изображение.

Если вам не нужен фон, просто не рисуйте его. Вы в полном контроле. Размер не имеет большого значения, потому что вы обычно знаете, насколько большими должны быть ваши иконки. В документации говорится, что передний план и фон должны быть 108dp, так что вы можете просто уменьшить масштаб вашего чертежа. Если на переднем плане / на заднем плане используется векторная графика, то размер действительно не имеет значения, так как вы можете просто нарисовать их так, как вам нравится.

Если вы растеризуете передний план, то вы можете сделать собственный рисунок, как показано выше, или выбрать Canvas#drawBitmap(...), который также предлагает несколько вариантов рисования растрового изображения, в том числе для передачи в матрицу преобразования или просто некоторые границы.

Если вы не растеризируете свой чертеж, вы также можете использовать drawable.setBounds(x1, y1, x2, y2), где вы можете установить границы того, где рисовать должен рисовать сам. Это также должно работать.

В каком случае именно это полезно использовать IconCompat.createWithAdaptiveBitmap()? Было написано, что "если вы создаете динамический ярлык с использованием растрового изображения, вы можете найти в IconCompat.createWithAdaptiveBitmap () библиотеки поддержки 26.0.0-beta2 полезную гарантию того, что растровое изображение правильно маскируется для соответствия другим адаптивным значкам"., но я не понимаю, в каких случаях это полезно.

ShortCutInfo.Builder имеет setIcon(Icon icon) метод, где вам нужно передать его. (То же самое относится и к версиям Compat)

Похоже, что Icon используется для управления видом растрового изображения, которое передается в виде значка. Прямо сейчас я не мог найти другое использование для Icon. Я не думаю, что вы бы использовали это при создании лаунчера.


Больше информации, отражающей последний комментарий

Оберните ли вы класс AdaptiveIconDrawable своим собственным объектом? Я просто хочу как-то преобразовать его во что-то, что я могу использовать, и в ImageView, и в растровое изображение, и я хочу управлять формой, используя все формы, которые я показал на скриншоте выше. Как бы я это сделал?

Если вы перейдете по ссылкам выше, вы можете увидеть обычай 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, над композицией, наследовать и просто рисовать фигуру, которая вам нравится.

Я знаю два способа создать значок нестандартной формы из AdaptiveIconDrawable. Однако я считаю, что Google следует обнародовать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;

}

Второй способ (тот, который мне нравится больше всего, потому что он позволяет кэшировать растровое изображение маски в случае необходимости, используя несколько раз, избегая перераспределения Bitmap, Canvas, BitmapShader и Paint). Если вы не поняли, проверьте эту ссылку:

@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;
}

Я предпочитаю второй способ, но лучший зависит от использования. Если сформирована только одна иконка, то первая сделает свою работу. Однако для нескольких значков лучше использовать второй. Поделитесь своими мыслями

Хорошо, у меня есть кое-что для работы, но по какой-то причине внутренний значок кажется меньше, чем то, что сделано с AdaptiveIconDrawable. Также по какой-то причине это повлияло на исходный AdaptiveIconDrawable (даже если я использовалmutateна любом чертеже, который я использовал), поэтому мне пришлось создать новый, чтобы продемонстрировать исходный и новый. Еще одно небольшое раздражение заключается в том, что для создания замаскированного растрового изображения мне нужно было иметь 2 экземпляра Bitmap (drawable, преобразованный в один, и также нужен был вывод).

Интересно, можно ли преобразовать объект для рисования непосредственно в Bitmap/Drawable с заданной формой, поэтому я спросил об этом здесь.

Итак, предположим, у вас есть Pathпример. Вы можете получить его с помощью функции AdaptiveIconDrawable.getIconMask (которая является одной из систем), или вы можете создать его самостоятельно, например, тот, который используется здесь (репозиторий здесь) или здесь.

Если кто-нибудь знает, как решить эти проблемы, о которых я упоминал выше (меньший передний план и влияет на исходную возможность рисования и, возможно, на лучшее преобразование), сообщите мне. На данный момент вы можете использовать это решение или библиотеку, как здесь.

Теперь предположим, что вы получили экземпляр 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
                }
            }
        }
    }
}

Я сделал обычай ImageView который может иметь путь, установленный для обрезки фона / рисования и применения правильной тени с помощью настраиваемого поставщика контура, который включает поддержку чтения системных настроек (как подтверждено на моих эмуляторах Pixel 4/, изменение формы значка системы распространяется на мое приложение.)

Посмотреть:

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())

Пример:

Запуски имеют гораздо меньше ограничений, чем приложения, поэтому они могут использовать другие подходы, но одно решение было красиво продемонстрировано на Adaptive Icon Playground Ника Батчера.

Вероятно, класс, который вас интересует, - это Adaptive Icon View, который отображает адаптированные версии значка, создавая растр каждого слоя с фоном в виде растрового изображения холста, а затем рисуя эти слои в виде прямоугольников со скругленными углами для реализации отсечения.

Связанный репозиторий будет гораздо более информативным и будет содержать примеры того, как преобразовать слой для эффектов движения и т. Д., Но вот основной псевдокод для "адаптации иконки" в представлении изображения:

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)
}

В любом случае, сегодня есть гораздо более простой способ решить эту проблему, используяcom.google.android.material.imageview.ShapeableImageView:

      fun ShapeableImageView.setAdaptiveIcon(drawable: Drawable?) =
    setImageDrawable(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
                         && drawable is AdaptiveIconDrawable) {
        background = drawable.background
        drawable.foreground
    } else {
        drawable 
    })

Вот и все. Просто установите любую форму, которая вам нужна, наShapeableImageViewи значок будет соответственно обрезан.

Примечание: АнAdaptiveIconDrawableсогласно документации, слой переднего плана обычно вставляется на 18dp. В зависимости от вашего варианта использования вы можете установитьcontentPaddingпредставления изображения на -18dp, чтобы компенсировать это.

Так как Launcher - это просто Activity, вы можете нарисовать что угодно. Вы можете рисовать значки приложений, например пони, которые работают на красивых анимированных облаках. Это твой мир, который подчиняется только твоим правилам.

Далее... В мире программирования нет магии. Если вы столкнулись с магией, просто используйте декомпиляторы (с Java это очень просто), найдите код, отвечающий за магию, документируйте его и напишите отличный пост в блоге о том, как работает эта магия.

Если у вас есть экземпляр AdaptiveIconDrawable, как вы его формируете, чтобы он имел круглую форму, прямоугольник, прямоугольник с закругленными углами, разрыв и т. Д.?

Вы можете использовать AdaptiveIconDrawable.getBackground() и добавить к нему любую маску. На самом деле, вы можете делать все что угодно с помощью значка, AdaptiveIconDrawable - это просто способ, где вы можете легко разделить передний план и фон без сложных фильтров или нейронных сетей. Добавьте параллакс, анимацию и многие другие эффекты, теперь у вас есть 2 слоя для него.

Другие вопросы по тегам