Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
289 views
in Technique[技术] by (71.8m points)

android - How do launchers change the shape of an adaptive icon, including removal of background?

Background

Starting from Android O, apps can have adaptive icons, which are 2 layers of drawables: foreground and a background. The background is a mask that gets to be a shape of the launcher/user's choice, while the OS has a default shape for it too.

Here's an example of what Nova Launcher allows to do:

enter image description here

As you can see, it allows not only to choose which shape to use, but also avoid a shape at all (in "prefer legacy icons").

Here are some links about it:

The problem

While I know how to create a AdaptiveIconDrawable instance, and I'm aware of the wizard that helps creating one for the current app, I don't get how, given an AdaptiveIconDrawable instance, launchers change the shape.

Not only that, but I remember I saw a launcher or two that allows to not have any shape.

Sadly I can't find any information about this part, maybe because this is a relatively very new feature. There isn't even a keyword for it here on StackOverflow.

What I've tried

I tried reading about adaptive icons, but couldn't find a reference to the receiver side.

I know it has the 2 drawables within it:

I know, at least, how to get an AdaptiveIconDrawable instance out of a third party app (assuming it has one) :

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

The questions

  1. Given a AdaptiveIconDrawable instance, how do you shape it, to be of a circular shape, rectangle, rounded rectangle, tear, and so on?

  2. How do I remove the shape and still have a valid size of the icon (using its foreground drawable in it) ? The official size of an app icon for launchers is 48 dp, while the official ones for AdaptiveIconDrawable inner drawables are 72dp (foreground), 108dp (background). I guess this would mean taking the foreground drawable, resize it somehow, and convert to a bitmap.

  3. In which case exactly is it useful to use IconCompat.createWithAdaptiveBitmap() ? It was written that "If you’re building a dynamic shortcut using a Bitmap, you might find the Support Library 26.0.0-beta2’s IconCompat.createWithAdaptiveBitmap() useful in ensuring that your Bitmap is masked correctly to match other adaptive icons." , but I don't get which cases it's useful for.


EDIT: In order to create a bitmap out of the foreground part of the adaptive icon, while resizing to a proper size, I think this could be a good solution:

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)

Might be able to avoid having 2 bitmaps at the same time, but this is ok I think.

About the creation of a shaped drawable of various types, I'm still not sure how to do it. Only solution I've seen by the answers below is of using a rounded rectangle or a circle, but there are other shapes (for example the tear) that can come to mind.


EDIT: I was told as some point by Google (here) that I should use AdaptiveIconDrawable.getIconMask(), but I wasn't given any further information. However, I've found a nice article about this here.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

I don't get how, given an AdaptiveIconDrawable instance, launchers change the shape.

Launchers are just apps, so they simply draw the background in the shape they want (or the user selected) and then draw the foreground on top.

I don't have a sample project of my own, but Nick Butcher made a great sample project and series of blog posts: AdaptiveIconPlayground.


Given a AdaptiveIconDrawable instance, how do you shape it, to be of a circular shape, rectangle, rounded rectangle, tear, and so on?

The simplest way is to rasterize the drawable and draw the bitmap using a shader like it is done in Nick's 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)

How do I remove the shape and still have a valid size of the icon (using its foreground drawable in it) ? The official size of an app icon for launchers is 48 dp, while the official ones for AdaptiveIconDrawable inner drawables are 72dp (foreground), 108dp (background). I guess this would mean taking the foreground drawable, resize it somehow, and convert to a bitmap.

If you don't want a background, just don't draw it. You're in full control. The size does not really matter, because you usually know how big your icons should be drawn. The documentation states that foreground and background should be 108dp, so you can simply downscale your drawing. If foreground/background use vector graphics, then size really does not matter, as you can just draw them however big you like.

If you rasterize the foreground, then you can do custom drawing as seen above, or choose Canvas#drawBitmap(...), which also offers multiple options to draw a Bitmap, including to pass in a transformation matrix, or simply some bounds.

If you don't rasterize your drawable you can also use drawable.setBounds(x1, y1, x2, y2), where you can set the bounds on where the drawable should draw itself. This should also work.

In which case exactly is it useful to use IconCompat.createWithAdaptiveBitmap() ? It was written that "If you’re building a dynamic shortcut using a Bitmap, you might find the Support Library 26.0.0-beta2’s IconCompat.createWithAdaptiveBitmap() useful in ensuring that your Bitmap is masked correctly to match other adaptive icons." , but I don't get which cases it's useful for.

ShortCutInfo.Builder has a setIcon(Icon icon) method where you need to pass it in. (And the same applies for the compat versions)

It seems that Icon is used to have control over the kind of Bitmap that gets passed in as an icon. Right now I could not find any other usage for Icon. I don't think that you would use this when creating a launcher.


More information reflecting the last comment

Do you wrap the AdaptiveIconDrawable class with your own drawable? I just want to convert it somehow to something I can use, to both an ImageView and a Bitmap, and I wish to control the shape, using all shapes I've shown on the screenshot above. How would I do it?

If you follow the links above you can see a custom AdaptiveIconView that draws the AdaptiveIconDrawable, so doing a custom view is definitely an option, but everything mentioned can be moved just as easily into a custom Drawable, which you then could also use with a basic ImageView.

You can achieve the various different backgrounds by using the methods available on Canvas along with a BitmapShader as shown above, e.g. additionally to drawRoundRect we would have

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

To switch between background shapes you could use anything from if/else, over composition, to inheritance, and just draw the shape you like.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

1.4m articles

1.4m replys

5 comments

57.0k users

...