|
自適應圖標主要用於在發射器上可以根據不同的配置顯示不同形狀的圖標,可以顯示圓形方形等形狀。
Adaptive Icons介绍對應Adaptive Icons的介紹google開發者和各路翻譯過來的網址很多,這裡貼下兩個網址僅供參考。
官方地址
翻譯地址
主要說明應用適應Adaptive Icons的注意點和方式
1。當應用targetsdk>=26,adaptive icon就会自动生效,即使資源中並並沒有指定為自適應圖標,但實際上使用自適應圖標,圖片資源是要重新修改的,如果不改,雖然自適應會生效,但效果可能不好。
如何讓應用的圖標效果更好呢?
定義一個XML作為繪製
- <?xml version="1.0" encoding="utf-8"?>
- <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
- <background android:drawable="@drawable/ic_launcher_background" />
- <foreground android:drawable="@drawable/ic_launcher_foreground" />
- </adaptive-icon>
複製代碼
背景是背景圖片,前景是前景圖片
也可以這樣:
- <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
- <background android:drawable="@color/ic_contacts_launcher_background"/>
- <foreground android:drawable="@mipmap/ic_contacts_launcher_foreground"/>
- </adaptive-icon>
複製代碼
背景可以使用顏色定義。
2.如果應用的targetsdk <26,想用自適應圖標的話,就需要使用上述的xml,可以在mipmap-anydpi-v26文件夾中配置。
圖片中心72 x 72 dp範圍為可視範圍。系統會保留四周外的36dp範圍用於生成有趣的視覺效果(如視差效果和跳動) 。
AdaptiveIconDrawable代碼走讀
Adaptive Icon實現方式通過上述xml來定義,我們來看下他的源碼實現方式。
首先它同BitmapDrawable,AnimationDrawable等都是繼承了Drawable,核心功能就是實現drawable的draw方法。
首先看下它的構造方法:
- /**
- * Constructor used to dynamically create this drawable.
- *
- * @param backgroundDrawable drawable that should be rendered in the background
- * @param foregroundDrawable drawable that should be rendered in the foreground
- */
- public AdaptiveIconDrawable(Drawable backgroundDrawable,
- Drawable foregroundDrawable) {
- this((LayerState)null, null);
- if (backgroundDrawable != null) {
- addLayer(BACKGROUND_ID, createChildDrawable(backgroundDrawable));
- }
- if (foregroundDrawable != null) {
- addLayer(FOREGROUND_ID, createChildDrawable(foregroundDrawable));
- }
- }
複製代碼
這個方法裡面獲取前景圖片和背景圖片。
我們再看下這個的實現方法
- /**
- * The one constructor to rule them all. This is called by all public
- * constructors to set the state and initialize local properties.
- */
- AdaptiveIconDrawable(@Nullable LayerState state, @Nullable Resources res) {
- mLayerState = createConstantState(state, res);
- if (sMask == null) {
- sMask = PathParser.createPathFromPathData(
- Resources.getSystem().getString(R.string.config_icon_mask));
- }
- mMask = PathParser.createPathFromPathData(
- Resources.getSystem().getString(R.string.config_icon_mask));
- mMaskMatrix = new Matrix();
- mCanvas = new Canvas();
- mTransparentRegion = new Region();
- }
複製代碼
這個方法我們重點關注一下mmask指定,這個變量就是代表的圖標的形狀。我們可以看到這個值獲取方式Resources.getSystem()。的getString(R.string.config_icon_mask)
查看這個config_icon_mask的值
- <!-- Specifies the path that is used by AdaptiveIconDrawable class to crop launcher icons. -->
- <string name="config_icon_mask" translatable="false">"M50,0L92,0C96.42,0 100,4.58 100 8L100,92C100, 96.42 96.42 100 92 100L8 100C4.58, 100 0 96.42 0 92L0 8 C 0 4.42 4.42 0 8 0L50 0Z"</string>
複製代碼
M,C,L等基本語法可以到網上搜索下。
也就是默認情況下獲取自適應圖標默認取得就是該形狀的圖標。這個應該是個圓形的樣式。
然後我們再看下AdaptiveIconDrawable的繪製方法,具體
- private void updateMaskBoundsInternal(Rect b) {
- mMaskMatrix.setScale(b.width() / MASK_SIZE, b.height() / MASK_SIZE);
- sMask.transform(mMaskMatrix, mMask);
- if (mMaskBitmap == null || mMaskBitmap.getWidth() != b.width() ||
- mMaskBitmap.getHeight() != b.height()) {
- mMaskBitmap = Bitmap.createBitmap(b.width(), b.height(), Bitmap.Config.ALPHA_8);
- mLayersBitmap = Bitmap.createBitmap(b.width(), b.height(), Bitmap.Config.ARGB_8888);
- }
- // mMaskBitmap bound [0, w] x [0, h]
- mCanvas.setBitmap(mMaskBitmap);
- mPaint.setShader(null);
- mCanvas.drawPath(mMask, mPaint);
- // mMask bound [left, top, right, bottom]
- mMaskMatrix.postTranslate(b.left, b.top);
- mMask.reset();
- sMask.transform(mMaskMatrix, mMask);
- // reset everything that depends on the view bounds
- mTransparentRegion.setEmpty();
- mLayersShader = null;
- }
-
- @Override
- public void draw(Canvas canvas) {
- if (mLayersBitmap == null) {
- return;
- }
- if (mLayersShader == null) {
- mCanvas.setBitmap(mLayersBitmap);
- mCanvas.drawColor(Color.BLACK);
- for (int i = 0; i < mLayerState.N_CHILDREN; i++) {
- if (mLayerState.mChildren[i] == null) {
- continue;
- }
- final Drawable dr = mLayerState.mChildren[i].mDrawable;
- if (dr != null) {
- dr.draw(mCanvas);
- }
- }
- mLayersShader = new BitmapShader(mLayersBitmap, TileMode.CLAMP, TileMode.CLAMP);
- mPaint.setShader(mLayersShader);
- }
- if (mMaskBitmap != null) {
- Rect bounds = getBounds();
- canvas.drawBitmap(mMaskBitmap, bounds.left, bounds.top, mPaint);
- }
- }
複製代碼
意思就是將兩張圖層抽拉先繪製上去,再根據的getBounds區域將mMaskBitmap繪製上去。當然之前還有一些區域的縮放等操作。
還得了解下BitmapShader著色器的使用方法。
啟動設置圖標形狀
先看下SettingsActivity.java中的菜單實現
- Preference iconShapeOverride = findPreference(IconShapeOverride.KEY_PREFERENCE);
- if (iconShapeOverride != null) {
- if (IconShapeOverride.isSupported(getActivity())) {
- IconShapeOverride.handlePreferenceUi((ListPreference) iconShapeOverride);
- } else {
- getPreferenceScreen().removePreference(iconShapeOverride);
- }
- }
複製代碼
由此可以看到則isSupported方法是是否支持設置圖標形狀的判斷條件。
- public static boolean isSupported(Context context) {
- ///1.判断系统SDK 版本是否>=26
- if (!Utilities.isAtLeastO()) {
- return false;
- }
- // Only supported when developer settings is enabled
- ///2.是否打开了开发者选项。如果开发者选项没打开,就看不到这个菜单。
- if (Settings.Global.getInt(context.getContentResolver(),
- Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 1) {
- return false;
- }
- try {
- if (getSystemResField().get(null) != Resources.getSystem()) {
- // Our assumption that mSystem is the system resource is not true.
- /// 3.大概意思就是获取不到mSystem,如果获取不到,说明当前系统存在问题
- return false;
- }
- } catch (Exception e) {
- // Ignore, not supported
- return false;
- }
- ///4. 获取系统中config_icon_mask的resource id
- return getConfigResId() != 0;
- }
複製代碼
注意點就是android 8.0設備要打開開發者選項一般就會有此功能,說明支持AdaptiveIcon。
菜單出現後,我們選擇其中一種形狀來設置。
- <!-- Values for icon shape overrides. These should correspond to entries defined
- in icon_shape_override_paths_names -->
- <string-array translatable="false" name="icon_shape_override_paths_values">
- <item></item>
- <item>M50,0L100,0 100,100 0,100 0,0z</item>
- <item>M50,0 C10,0 0,10 0,50 0,90 10,100 50,100 90,100 100,90 100,50 100,10 90,0 50,0 Z</item>
- <item>M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0</item>
- <item>M50,0A50,50,0,0 1 100,50 L100,85 A15,15,0,0 1 85,100 L50,100 A50,50,0,0 1 50,0z</item>
- </string-array>
- <string-array translatable="false" name="icon_shape_override_paths_names">
- <!-- Option to not change the icon shape on home screen. [CHAR LIMIT=50] -->
- <item>@string/icon_shape_system_default</item>
- <item>Square</item>
- <item>Squircle</item>
- <item>Circle</item>
- <item>Teardrop</item>
- </string-array>
複製代碼
打開可以看到一個形狀對應的值就是一個矢量圖的字符串值。
- private static class PreferenceChangeHandler implements OnPreferenceChangeListener {
- private final Context mContext;
- private PreferenceChangeHandler(Context context) {
- mContext = context;
- }
- @Override
- public boolean onPreferenceChange(Preference preference, Object o) {
- String newValue = (String) o;
- if (!getAppliedValue(mContext).equals(newValue)) {
- // Value has changed
- ProgressDialog.show(mContext,
- null /* title */,
- mContext.getString(R.string.icon_shape_override_progress),
- true /* indeterminate */,
- false /* cancelable */);
- new LooperExecuter(LauncherModel.getWorkerLooper()).execute(
- new OverrideApplyHandler(mContext, newValue));
- }
- return false;
- }
- }
-
- private static class OverrideApplyHandler implements Runnable {
- private final Context mContext;
- private final String mValue;
- private OverrideApplyHandler(Context context, String value) {
- mContext = context;
- mValue = value;
- }
- @Override
- public void run() {
- // Synchronously write the preference.
- prefs(mContext).edit().putString(KEY_PREFERENCE, mValue).commit();
- // Clear the icon cache.
- LauncherAppState.getInstance(mContext).getIconCache().clear();
- // Wait for it
- try {
- Thread.sleep(PROCESS_KILL_DELAY_MS);
- } catch (Exception e) {
- Log.e(TAG, "Error waiting", e);
- }
- // Schedule an alarm before we kill ourself.
- Intent homeIntent = new Intent(Intent.ACTION_MAIN)
- .addCategory(Intent.CATEGORY_HOME)
- .setPackage(mContext.getPackageName())
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- PendingIntent pi = PendingIntent.getActivity(mContext, RESTART_REQUEST_CODE,
- homeIntent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
- mContext.getSystemService(AlarmManager.class).setExact(
- AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 50, pi);
- // Kill process
- android.os.Process.killProcess(android.os.Process.myPid());
- }
- }
複製代碼
設置的時候執行上面代碼,主要將設置的保存到本地,清除圖標緩存,然後重啟發射。
如何改變發射器上的圖標的
我們再看下上面設置的圖標形狀的值到底是怎麼使用的,如何使圖標變化的
我們找到LauncherProvider的onCreate方法裡面使用的地方。
- IconShapeOverride.apply(getContext());
複製代碼
看看這個申請方法:
- private static int getConfigResId() {
- return Resources.getSystem().getIdentifier("config_icon_mask", "string", "android");
- }
-
- public static void apply(Context context) {
- if (!Utilities.isAtLeastO()) {
- return;
- }
- String path = getAppliedValue(context);
- if (TextUtils.isEmpty(path)) {
- return;
- }
- if (!isSupported(context)) {
- return;
- }
- // magic
- try {
- Resources override =
- new ResourcesOverride(Resources.getSystem(), getConfigResId(), path);
- getSystemResField().set(null, override);
- } catch (Exception e) {
- Log.e(TAG, "Unable to override icon shape", e);
- // revert value.
- prefs(context).edit().remove(KEY_PREFERENCE).apply();
- }
- }
複製代碼
其中ResourcesOverride是繼承了資源,並且重寫了的getString方法
- private static class ResourcesOverride extends Resources {
- private final int mOverrideId;
- private final String mOverrideValue;
- @SuppressWarnings("deprecated")
- public ResourcesOverride(Resources parent, int overrideId, String overrideValue) {
- super(parent.getAssets(), parent.getDisplayMetrics(), parent.getConfiguration());
- mOverrideId = overrideId;
- mOverrideValue = overrideValue;
- }
- @NonNull
- @Override
- public String getString(int id) throws NotFoundException {
- if (id == mOverrideId) {
- return mOverrideValue;
- }
- return super.getString(id);
- }
- }
複製代碼
再看一下getSystemResField方法
- private static Field getSystemResField() throws Exception {
- Field staticField = Resources.class.getDeclaredField("mSystem");
- staticField.setAccessible(true);
- return staticField;
- }
複製代碼
這個方法是反射系統資源中mSystem變量。
上面大概的意思就是Launcher中將Resources的mSystem設置成了ResourcesOverride對象,
也就是說Resources的getSystem方法獲取的是我們重寫的ResourcesOverride,當調用getString方法的時候,走的也是重寫的方法.getString方法裡面判斷瞭如果string id是config_icon_mask這個的時候,返回我們傳入的mOverrideValue,這個mOverrideValue就是用戶選擇的圖標形狀值。
- /**
- * Return a global shared Resources object that provides access to only
- * system resources (no application resources), and is not configured for
- * the current screen (can not use dimension units, does not change based
- * on orientation, etc).
- */
- public static Resources getSystem() {
- synchronized (sSync) {
- Resources ret = mSystem;
- if (ret == null) {
- ret = new Resources();
- mSystem = ret;
- }
- return ret;
- }
- }
複製代碼
現在回頭看下AdaptiveIconDrawable的構造方法:
- /**
- * The one constructor to rule them all. This is called by all public
- * constructors to set the state and initialize local properties.
- */
- AdaptiveIconDrawable(@Nullable LayerState state, @Nullable Resources res) {
- mLayerState = createConstantState(state, res);
- if (sMask == null) {
- sMask = PathParser.createPathFromPathData(
- Resources.getSystem().getString(R.string.config_icon_mask));
- }
- mMask = PathParser.createPathFromPathData(
- Resources.getSystem().getString(R.string.config_icon_mask));
- mMaskMatrix = new Matrix();
- mCanvas = new Canvas();
- mTransparentRegion = new Region();
- }
複製代碼
此方法的Resources.getSystem()。的getString(R.string.config_icon_mask),通過的getString方法,如果ID是config_icon_mask,則返回的是mOverrideValue,mOverrideValue就是上面5種裡面的一種。
因此,啟動器獲取應用圖標的時候時候,如果該應用是支持AdaptiveIcon的話,返回的圖標就是根據形狀裁剪出來的AdaptiveIconDrawable,啟動器從系統拿到的圖標已經是想要的形狀圖標了。
看下我們啟動是如何獲取應用圖標的
- public Drawable getFullResIcon(LauncherActivityInfo info) {
- return mIconProvider.getIcon(info, mIconDpi);
- }
-
- public Drawable getIcon(LauncherActivityInfo info, int iconDpi) {
- return info.getIcon(iconDpi);
- }
複製代碼
最終調用到LauncherActivityInfo的方法調用getIcon
- /**
- * Returns the icon for this activity, without any badging for the profile.
- * @param density The preferred density of the icon, zero for default density. Use
- * density DPI values from {@link DisplayMetrics}.
- * @see #getBadgedIcon(int)
- * @see DisplayMetrics
- * @return The drawable associated with the activity.
- */
- public Drawable getIcon(int density) {
- // TODO: Go through LauncherAppsService
- final int iconRes = mActivityInfo.getIconResource();
- Drawable icon = null;
- // Get the preferred density icon from the app's resources
- if (density != 0 && iconRes != 0) {
- try {
- final Resources resources
- = mPm.getResourcesForApplication(mActivityInfo.applicationInfo);
- icon = resources.getDrawableForDensity(iconRes, density);
- } catch (NameNotFoundException | Resources.NotFoundException exc) {
- }
- }
- // Get the default density icon
- if (icon == null) {
- icon = mActivityInfo.loadIcon(mPm);
- }
- return icon;
- }
複製代碼
經過試驗,系統返回的繪製,就已經是我們想要的設置的形狀圖標了。
演示驗證
下面我自己參考上述的代碼,寫個獨立的演示,看看獲取的圖標。我們可以傳任意形狀的圖形,看看返回的圖顯示情況。
我們將上面的寫在一個輔助類中代碼如下:
- /**
- * Created by LeongAndroid on 2017/11/9.
- */
- @TargetApi(Build.VERSION_CODES.O)
- public class IconShapeOverrideHelper {
- /**
- * 设置应用的新Resource
- * @param path
- */
- public static void apply(String path) {
- try {
- Resources override =
- new ResourcesOverride(Resources.getSystem(), getConfigResId(), path);
- getSystemResField().set(null, override);
- } catch (Exception e) {
- // revert value.
- Log.d("IconShapeHelper", "apply exception "+e);
- }
- }
- private static Field getSystemResField() throws Exception {
- Field staticField = Resources.class.getDeclaredField("mSystem");
- staticField.setAccessible(true);
- return staticField;
- }
- private static int getConfigResId() {
- return Resources.getSystem().getIdentifier("config_icon_mask", "string", "android");
- }
- private static class ResourcesOverride extends Resources {
- private final int mOverrideId;
- private final String mOverrideValue;
- @SuppressWarnings("deprecated")
- public ResourcesOverride(Resources parent, int overrideId, String overrideValue) {
- super(parent.getAssets(), parent.getDisplayMetrics(), parent.getConfiguration());
- mOverrideId = overrideId;
- mOverrideValue = overrideValue;
- }
- @NonNull
- @Override
- public String getString(int id) throws NotFoundException {
- if (id == mOverrideId) {
- return mOverrideValue;
- }
- return super.getString(id);
- }
- }
- public static Drawable getAppIcon(PackageManager pm, String packname){
- try {
- ApplicationInfo info = pm.getApplicationInfo(packname, 0);
- return info.loadIcon(pm);
- } catch (PackageManager.NameNotFoundException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- return null;
- }
- /**
- * 此方法可以获取应用图标的原始图
- * @param mPackageManager
- * @param packageName
- * @return
- */
- public static Bitmap getAppIcon2(PackageManager mPackageManager, String packageName) {
- try {
- Drawable drawable = mPackageManager.getApplicationIcon(packageName);
- if (drawable instanceof BitmapDrawable) {
- return ((BitmapDrawable) drawable).getBitmap();
- } else if (drawable instanceof AdaptiveIconDrawable) {
- Drawable backgroundDr = ((AdaptiveIconDrawable) drawable).getBackground();
- Drawable foregroundDr = ((AdaptiveIconDrawable) drawable).getForeground();
- Drawable[] drr = new Drawable[2];
- drr[0] = backgroundDr;
- drr[1] = foregroundDr;
- LayerDrawable layerDrawable = new LayerDrawable(drr);
- int width = layerDrawable.getIntrinsicWidth();
- int height = layerDrawable.getIntrinsicHeight();
- Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(bitmap);
- layerDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
- layerDrawable.draw(canvas);
- return bitmap;
- }
- } catch (PackageManager.NameNotFoundException e) {
- e.printStackTrace();
- }
- return null;
- }
- }
複製代碼
然後再寫個活性,通過標準API來獲取應用圖標,看看顯示什麼。
- public class AdaptiveIconActivity extends AppCompatActivity {
- private static final String TAG = "AdaptiveIcon";
- private ImageView imageView = null;
- private ImageView imageView1 = null;
- String patch = "M50,0A50,50,0,0 1 100,50 L100,85 A15,15,0,0 1 85,100 L50,100 A50,50,0,0 1 50,0z";
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.adaptive_icon_layout);
- IconShapeOverrideHelper.apply(patch);
- imageView = (ImageView)this.findViewById(R.id.image);
- imageView1 = (ImageView)this.findViewById(R.id.image1);
- ///直接用标准接口获取图标
- Drawable drawable = IconShapeOverrideHelper.getAppIcon(getPackageManager(), "com.leong.testandroido");
- imageView.setImageDrawable(drawable);
- ///图标原始
- Bitmap bitmap = IconShapeOverrideHelper.getAppIcon2(getPackageManager(), "com.leong.testandroido");
- Log.d(TAG, "origin bitmap w = "+bitmap.getWidth()+", h = "+bitmap.getHeight());
- imageView1.setImageBitmap(bitmap);
- }
- }
複製代碼
顯示效果如下:
效果圖
上面的圖就是我們返回的圖標,下面的圖是一個應用的原圖。
Demo源碼路徑:https://github.com/LeongAndroid/OLauncherNewFeature
總結
上面的方式我們可以設想下,如果Launcher3將設置的圖標形狀這個參數公開出去,那所有其他的應用都可以根據這個mMask來獲取跟Launcher3相同形狀的圖標。當然,這個就需要修改下Launcher3的代碼了,將設置的參數公開給外部應用。
文章出處https://www.jianshu.com/p/c7af54a361a2
https://www.jianshu.com/p/20df6c156f3d
|
|