之前我簡(jiǎn)單介紹過(guò)AdaptiveIcon的適配方式和實(shí)現(xiàn)原理避乏,今天主要介紹下在Launcher中是如何實(shí)現(xiàn)切換圖標(biāo)形狀的赵抢。
Launcher設(shè)置圖標(biāo)形狀
先看下SettingsActivity.java中的菜單實(shí)現(xiàn)
Preference iconShapeOverride = findPreference(IconShapeOverride.KEY_PREFERENCE);
if (iconShapeOverride != null) {
if (IconShapeOverride.isSupported(getActivity())) {
IconShapeOverride.handlePreferenceUi((ListPreference) iconShapeOverride);
} else {
getPreferenceScreen().removePreference(iconShapeOverride);
}
}
由此可以看到isSupported方法是是否支持設(shè)置圖標(biāo)形狀的判斷條件枣抱。
public static boolean isSupported(Context context) {
///1.判斷系統(tǒng)SDK 版本是否>=26
if (!Utilities.isAtLeastO()) {
return false;
}
// Only supported when developer settings is enabled
///2.是否打開了開發(fā)者選項(xiàng)解愤。如果開發(fā)者選項(xiàng)沒打開骤素,就看不到這個(gè)菜單家淤。
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异剥,如果獲取不到,說(shuō)明當(dāng)前系統(tǒng)存在問(wèn)題
return false;
}
} catch (Exception e) {
// Ignore, not supported
return false;
}
///4. 獲取系統(tǒng)中config_icon_mask的resource id
return getConfigResId() != 0;
}
注意點(diǎn)就是android 8.0設(shè)備要打開開發(fā)者選項(xiàng)一般就會(huì)有此功能絮重,說(shuō)明支持AdaptiveIcon.
菜單出現(xiàn)后冤寿,我們選擇其中一種形狀來(lái)設(shè)置。
<!-- 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>
打開可以看到一個(gè)形狀對(duì)應(yīng)的value 就是一個(gè)矢量圖的string值青伤。
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());
}
}
設(shè)置的時(shí)候執(zhí)行上面代碼督怜,主要將設(shè)置的保存到本地,清除圖標(biāo)緩存狠角,然后重啟launcher号杠。
如何改變Launcher上的圖標(biāo)的
我們?cè)倏聪律厦嬖O(shè)置的圖標(biāo)形狀的value到底是怎么使用的,如何使圖標(biāo)變化的
我們找到LauncherProvider 的onCreate方法里面使用的地方丰歌。
IconShapeOverride.apply(getContext());
看看這個(gè)apply方法:
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是繼承了Resources究流,并且重寫了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;
}
這個(gè)方法是反射系統(tǒng)Resources中mSystem變量辣吃。
上面大概的意思就是Launcher中將Resources 的mSystem設(shè)置成了ResourcesOverride對(duì)象,
也就是說(shuō)Resources的getSystem方法獲取的是我們重寫的ResourcesOverride芬探,當(dāng)調(diào)用getString方法的時(shí)候,走的也是重寫的方法厘惦。getString方法里面判斷了如果string id 是config_icon_mask這個(gè)的時(shí)候偷仿,返回我們傳入的mOverrideValue,這個(gè)mOverrideValue就是用戶選擇的圖標(biāo)形狀值宵蕉。
/**
* 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;
}
}
現(xiàn)在回頭看下AdaptiveIconDrawable的構(gòu)造方法:
/**
* 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)酝静,通過(guò)getString方法,如果id是config_icon_mask羡玛,則返回的是mOverrideValue别智,mOverrideValue就是上面5種里面的一種。
因此稼稿,Launcher獲取應(yīng)用圖標(biāo)的時(shí)候時(shí)候薄榛,如果該應(yīng)用是支持AdaptiveIcon的話,返回的圖標(biāo)就是根據(jù)形狀裁剪出來(lái)的AdaptiveIconDrawable让歼,Launcher從系統(tǒng)拿到的圖標(biāo)已經(jīng)是想要的形狀圖標(biāo)了敞恋。
看下我們Launcher是如何獲取應(yīng)用圖標(biāo)的
public Drawable getFullResIcon(LauncherActivityInfo info) {
return mIconProvider.getIcon(info, mIconDpi);
}
public Drawable getIcon(LauncherActivityInfo info, int iconDpi) {
return info.getIcon(iconDpi);
}
最終調(diào)用到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;
}
經(jīng)過(guò)試驗(yàn),系統(tǒng)返回的drawable谋右,就已經(jīng)是我們想要的設(shè)置的形狀圖標(biāo)了硬猫。
Demo驗(yàn)證
下面我自己參考上述的代碼,寫個(gè)獨(dú)立的demo改执,看看獲取的圖標(biāo)啸蜜。我們可以傳任意形狀的圖形,看看返回的圖顯示情況辈挂。
我們將上面的寫在一個(gè)helper類中衬横。代碼如下:
/**
* Created by LeongAndroid on 2017/11/9.
*/
@TargetApi(Build.VERSION_CODES.O)
public class IconShapeOverrideHelper {
/**
* 設(shè)置應(yīng)用的新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;
}
/**
* 此方法可以獲取應(yīng)用圖標(biāo)的原始圖
* @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;
}
}
然后再寫個(gè)Activity,通過(guò)標(biāo)準(zhǔn)api來(lái)獲取應(yīng)用圖標(biāo)呢岗,看看顯示什么冕香。
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);
///直接用標(biāo)準(zhǔn)接口獲取圖標(biāo)
Drawable drawable = IconShapeOverrideHelper.getAppIcon(getPackageManager(), "com.leong.testandroido");
imageView.setImageDrawable(drawable);
///圖標(biāo)原始
Bitmap bitmap = IconShapeOverrideHelper.getAppIcon2(getPackageManager(), "com.leong.testandroido");
Log.d(TAG, "origin bitmap w = "+bitmap.getWidth()+", h = "+bitmap.getHeight());
imageView1.setImageBitmap(bitmap);
}
}
顯示效果如下:
上面的圖就是我們返回的圖標(biāo),下面的圖是一個(gè)應(yīng)用的原圖后豫。
Demo 源碼路徑:https://github.com/LeongAndroid/OLauncherNewFeature
總結(jié)
上面的方式我們可以設(shè)想下悉尾,如果Launcher3 將設(shè)置的圖標(biāo)形狀這個(gè)參數(shù)公開出去,那所有其他的應(yīng)用都可以根據(jù)這個(gè)mMask來(lái)獲取跟Launcher3相同形狀的圖標(biāo)挫酿。當(dāng)然构眯,這個(gè)就需要修改下Launcher3的代碼了,將設(shè)置的參數(shù)公開給外部應(yīng)用早龟。
由于作者本人能力有限惫霸,如果文中有錯(cuò)誤的地方猫缭,歡迎指正,十分感謝耙嫉辍2碌ぁ!