Как средства запуска изменяют форму адаптивного значка, в том числе удаляют фон?
Фон
Начиная с Android O, приложения могут иметь адаптивные значки, которые представляют собой 2 слоя рисования: передний план и фон. Фон - это маска, которая становится формой по выбору программы запуска / пользователя, в то время как ОС тоже имеет форму по умолчанию.
Вот пример того, что Nova Launcher позволяет делать:
Как вы можете видеть, он позволяет не только выбирать, какую фигуру использовать, но и вообще избегать фигуры (в разделе "предпочитать устаревшие значки").
Вот несколько ссылок об этом:
- https://www.youtube.com/watch?v=5MHFYfXno9c
- https://medium.com/@ianhlake/vectordrawable-adaptive-icons-3fed3d3205b5
Эта проблема
Хотя я знаю, как создать экземпляр AdaptiveIconDrawable, и мне известен мастер, помогающий создать его для текущего приложения, я не понимаю, как, учитывая экземпляр AdaptiveIconDrawable, средства запуска изменяют форму.
Мало того, но я помню, что видел одну или две пусковые установки, позволяющие не иметь никакой формы.
К сожалению, я не могу найти какую-либо информацию об этой части, возможно, потому что это относительно очень новая функция. Здесь даже нет ключевого слова для этого в Stackru.
Что я пробовал
Я попытался прочитать об адаптивных значках, но не смог найти ссылку на приемник.
Я знаю, что в нем есть 2 drawables:
- https://developer.android.com/reference/android/graphics/drawable/AdaptiveIconDrawable.html
- https://developer.android.com/reference/android/graphics/drawable/AdaptiveIconDrawable.html
Я знаю, по крайней мере, как получить экземпляр 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
Вопросы
Если у вас есть экземпляр AdaptiveIconDrawable, как вы его формируете, чтобы он имел круглую форму, прямоугольник, прямоугольник с закругленными углами, разрыв и т. Д.?
Как мне удалить фигуру и при этом иметь действительный размер значка (используя нарисованный на нем передний план)? Официальный размер иконки приложения для пусковых установок составляет 48 dp, в то время как официальные размеры для внутренних Drawables AdaptiveIconDrawable - 72dp (на переднем плане), 108dp (на заднем плане). Я предполагаю, что это будет означать создание переднего плана для рисования, изменение его размера и преобразование в растровое изображение.
В каком случае именно это полезно использовать
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 слоя для него.