从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
给定一个AdaptiveIconDrawable实例,如何将其塑造成圆形,矩形,圆角矩形,撕裂等?
如何删除形状并且仍然具有图标的有效大小(使用其中的前景可绘制)?发射器的应用程序图标的官方大小为48 dp,而AdaptiveIconDrawable内部drawable的官方大小为72dp(前景),108dp(背景)。我想这意味着将前景绘制为可绘制,以某种方式调整大小,并转换为位图。
在哪种情况下使用IconCompat.createWithAdaptiveBitmap()
确实有用吗?有人写道:“如果您使用Bitmap构建动态快捷方式,您可能会发现支持库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个位图,但我认为这是可以的。
关于各种类型的塑形抽屉的制作,我仍然不知道该怎么做。我在下面的答案中看到的唯一解决方案是使用圆角矩形或圆形,但还有其他形状(例如撕裂)可以想到。
答案 0 :(得分:1)
在给定AdaptiveIconDrawable实例的情况下,我不知道发射器是如何改变形状的。
启动器只是应用程序,所以它们只是以他们想要的形状(或用户选择的)绘制背景,然后在前面绘制前景。
我没有自己的示例项目,但是Nick Butcher制作了一个很棒的示例项目和一系列博客文章:AdaptiveIconPlayground。
给定一个AdaptiveIconDrawable实例,你如何塑造它,形成一个圆形,矩形,圆角矩形,撕裂等等?
最简单的方法是栅格化drawable并绘制bitmap using a shader,就像在Nick 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内部drawable的官方大小为72dp(前景),108dp(背景)。我想这意味着将前景绘制为可绘制,以某种方式调整大小,然后转换为位图。
如果你不想要背景,那就不要画画。你完全掌控了。大小并不重要,因为您通常知道应该绘制多大的图标。文档说明前景和背景应该是108dp,因此您可以简单地缩小绘图。如果前景/背景使用矢量图形,那么尺寸确实无关紧要,因为你可以随心所欲地绘制它们。
如果你对前景进行光栅化,那么你可以如上所示进行自定义绘制,或者选择Canvas#drawBitmap(...)
,它还提供了多个绘制位图的选项,包括传入变换矩阵或简单的一些边界。
如果你没有光栅化你的绘图,你也可以使用drawable.setBounds(x1, y1, x2, y2)
,在那里你可以设置绘图应该绘制自己的边界。这也应该有用。
在哪种情况下,使用IconCompat.createWithAdaptiveBitmap()确实有用吗?编写了&#34;如果您使用位图构建动态快捷方式,您可能会发现支持库26.0.0-beta2的IconCompat.createWithAdaptiveBitmap()可用于确保正确屏蔽您的位图以匹配其他自适应图标&#34; ,但我不知道哪些案例对它有用。
ShortCutInfo.Builder
有setIcon(Icon icon)
方法,您需要将其传入。(同样适用于compat版本)
似乎Icon用于控制作为图标传入的Bitmap的类型。现在我找不到Icon的任何其他用法。我不认为你在创建启动器时会使用它。
反映最后评论的更多信息
您是否使用自己的drawable包装AdaptiveIconDrawable类?我只是想以某种方式将它转换为我可以使用的东西,同时转换为ImageView和Bitmap,并且我希望使用我在上面的屏幕截图中显示的所有形状来控制形状。我该怎么做?
如果你按照上面的链接,你可以看到绘制AdaptiveIconDrawable的自定义AdaptiveIconView
,所以做一个自定义视图绝对是一个选项,但所提到的一切都可以轻松地移动到自定义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,over composition,继承等任何内容,只需绘制您喜欢的形状。
答案 1 :(得分:0)
发射器的限制比应用程序少得多,因此他们可能会使用其他方法,但在Nick Butcher的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)
}
答案 2 :(得分:0)
好的,我有一些工作要做,但是由于某种原因,内部图标似乎比AdaptiveIconDrawable的图标小。同样由于某种原因,在某种程度上,它影响了原始的AdaptiveIconDrawable(即使我在使用的任何可绘制对象上使用了mutate
),所以我不得不创建一个新的来演示原始vs新的。另一个小麻烦是要创建被屏蔽的位图,我必须有2个位图实例(drawable转换为一个,也需要输出)。
我想知道是否可以将drawable直接转换为具有给定形状的Bitmap / Drawable,所以我问了这个here。
因此,假设您有一个Path
实例。您可以从AdaptiveIconDrawable.getIconMask函数(系统的其中之一)获得一个,也可以自己创建一个,例如使用过的here(存储库here)或{{ 3}}。
如果有人知道如何解决我上面提到的那些问题(前景变小并影响原始可绘制对象,并且可能会有更好的转换效果),请告诉我。目前,您既可以使用此解决方案,也可以使用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
}
}
}
}
}
答案 3 :(得分:0)
我知道两种从AdaptiveIconDrawable构建自定义形状图标的方法。不过,我认为Google应该公开公开AdaptiveIconDrawable.setMask(Path path)
方法:
第一种方法(与AOSP code完全相同):
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的重新分配)。如果您听不懂,请确保签出this link:
@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;
}
我更喜欢第二种方法,但是最好的方法取决于用法。如果仅塑造了一个图标,则第一个将完成工作。但是,对于多个图标,第二个图标更好。分享您的想法
答案 4 :(得分:-1)
由于Launcher只是一个Activity,你可以绘制任何东西。您可以绘制应用程序图标,如在美丽的动画云上运行的小马。这是你的世界,只遵守你的规则。
此外......编程领域没有任何魔力。如果你面对魔法,只需使用反编译器(使用Java非常容易),找到负责魔法的代码,记录它并写一篇关于这个魔法如何工作的精彩博客文章。
给定一个AdaptiveIconDrawable实例,你如何塑造它,成为一个 圆形,矩形,圆角矩形,撕裂等?
您可以使用AdaptiveIconDrawable.getBackground()并向其添加任何掩码。实际上,你可以用图标做任何你想做的事情,AdaptiveIconDrawable就是这样,你可以轻松地分割前景和背景,没有复杂的过滤器或神经网络。添加视差,动画和更多效果,现在你有2层。