摘要:本文主要针对客户需求将Launcher3删除风格从顶部修改为在图标旁显示。本文先从android 5.1,高通msm8916平台,对Launcher3的删除显示风格进行分析,然后在此基础上将新的删除风格添加到相应的代码中。
1.Launcher3删除流程分析
Launcher3原生的删除风格是长按图标,会在顶部中间出复现删除区域,通过拖动图标至删除区域实现接下来的删除操作。通过logcat分析,发现删除是调用系统的PackageInstaller.apk里的接口实现的,根据经验全局搜索Intent.ACTION_DELETE在Launcher.java中找到代码位置:
packages/apps/Launcher3/src/com/android/launcher3/Launcher.java
// returns true if the activity was started
boolean startApplicationUninstallActivity(ComponentName componentName, int flags) {
if ((flags & AppInfo.DOWNLOADED_FLAG) == 0) {
// System applications cannot be installed. For now, show a toast explaining that.
// We may give them the option of disabling apps this way.
int messageId = R.string.uninstall_system_app_text;
Toast.makeText(this, messageId, Toast.LENGTH_SHORT).show();
return false;
} else {
String packageName = componentName.getPackageName();
String className = componentName.getClassName();
Intent intent = new Intent(
Intent.ACTION_DELETE, Uri.fromParts("package", packageName, className));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
startActivity(intent);
return true;
}
}
找到最终实现删除的代码,就可以通过在方法入口处添加log追踪trace:
android.util.Log.d(TAG, "trace: ", new Exception());
packages/apps/Launcher3/src/com/android/launcher3/DeleteDropTarget.java
private void completeDrop(DragObject d) {
ItemInfo item = (ItemInfo) d.dragInfo;
boolean wasWaitingForUninstall = mWaitingForUninstall;
mWaitingForUninstall = false;
if (isAllAppsApplication(d.dragSource, item)) {
// Uninstall the application if it is being dragged from AppsCustomize
AppInfo appInfo = (AppInfo) item;
mLauncher.startApplicationUninstallActivity(appInfo.componentName, appInfo.flags);
} else if (isUninstallFromWorkspace(d)) {
ShortcutInfo shortcut = (ShortcutInfo) item;
if (shortcut.intent != null && shortcut.intent.getComponent() != null) {
final ComponentName componentName = shortcut.intent.getComponent();
final DragSource dragSource = d.dragSource;
int flags = AppInfo.initFlags(
ShortcutInfo.getPackageInfo(getContext(), componentName.getPackageName()));
mWaitingForUninstall =
mLauncher.startApplicationUninstallActivity(componentName, flags); // 此处调用
if (mWaitingForUninstall) {
final Runnable checkIfUninstallWasSuccess = new Runnable() {
@Override
public void run() {
mWaitingForUninstall = false;
String packageName = componentName.getPackageName();
List<ResolveInfo> activities =
AllAppsList.findActivitiesForPackage(getContext(), packageName);
boolean uninstallSuccessful = activities.size() == 0;
if (dragSource instanceof Folder) {
((Folder) dragSource).
onUninstallActivityReturned(uninstallSuccessful);
} else if (dragSource instanceof Workspace) {
((Workspace) dragSource).
onUninstallActivityReturned(uninstallSuccessful);
}
}
};
mLauncher.addOnResumeCallback(checkIfUninstallWasSuccess);
}
}
} else if (isWorkspaceOrFolderApplication(d)) {
LauncherModel.deleteItemFromDatabase(mLauncher, item);
} else if (isWorkspaceFolder(d)) {
// Remove the folder from the workspace and delete the contents from launcher model
FolderInfo folderInfo = (FolderInfo) item;
mLauncher.removeFolder(folderInfo);
LauncherModel.deleteFolderContentsFromDatabase(mLauncher, folderInfo);
} else if (isWorkspaceOrFolderWidget(d)) {
// Remove the widget from the workspace
mLauncher.removeAppWidget((LauncherAppWidgetInfo) item);
LauncherModel.deleteItemFromDatabase(mLauncher, item);
final LauncherAppWidgetInfo launcherAppWidgetInfo = (LauncherAppWidgetInfo) item;
final LauncherAppWidgetHost appWidgetHost = mLauncher.getAppWidgetHost();
if (appWidgetHost != null) {
// Deleting an app widget ID is a void call but writes to disk before returning
// to the caller...
new AsyncTask<Void, Void, Void>() {
public Void doInBackground(Void ... args) {
appWidgetHost.deleteAppWidgetId(launcherAppWidgetInfo.appWidgetId);
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null);
}
}
if (wasWaitingForUninstall && !mWaitingForUninstall) {
if (d.dragSource instanceof Folder) {
((Folder) d.dragSource).onUninstallActivityReturned(false);
} else if (d.dragSource instanceof Workspace) {
((Workspace) d.dragSource).onUninstallActivityReturned(false);
}
}
}
2. 添加新的删除风格
可见操作是在onDrop这类操作之后判断执行的。
通过trace我们确定了添加代码的位置在workspace.java中的startDrag方法中比较合理。
packages/apps/Launcher3/src/com/android/launcher3/Workspace.java
void startDrag(CellLayout.CellInfo cellInfo) {
View child = cellInfo.cell;
// Make sure the drag was started by a long press as opposed to a long click.
if (!child.isInTouchMode()) {
return;
}
mDragInfo = cellInfo;
child.setVisibility(INVISIBLE);
CellLayout layout = (CellLayout) child.getParent().getParent();
layout.prepareChildForDrag(child);
child.clearFocus();
child.setPressed(false);
final Canvas canvas = new Canvas();
// The outline is used to visualize where the item will land if dropped
mDragOutline = createDragOutline(child, canvas, DRAG_BITMAP_PADDING);
beginDragShared(child, this);
// modify @ + {
Object dragInfo = cellInfo.cell.getTag();
if (cellInfo.cell.getTag() instanceof ItemInfo) {
ItemInfo item = (ItemInfo) dragInfo;
if (item instanceof ShortcutInfo) {
ShortcutInfo shortcutInfo = (ShortcutInfo) item;
if (shortcutInfo.intent != null && shortcutInfo.intent.getComponent() != null) {
ComponentName componentName = shortcutInfo.intent.getComponent();
if (DeleteDropTarget.willAcceptDrop(child.getTag())) {
showDialog(cellInfo.cell, componentName);
}
}
}
}
// modify @ + }
}
// modify @ + {
private PopupWindow popupWindow;
private TextView tvDelete;
private void showDialog(View v, final ComponentName componentName) {
View popView = LayoutInflater.from(getContext()).inflate(R.layout.layout_long_click_dialog, null);
tvDelete=(TextView) popView.findViewById(R.id.delete_target_tv);
tvDelete.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (popupWindow.isShowing()) {
popupWindow.dismiss();
}
int flags = AppInfo.initFlags(
ShortcutInfo.getPackageInfo(getContext(), componentName.getPackageName()));
boolean mWaitingForUninstall =
mLauncher.startApplicationUninstallActivity(componentName, flags);
}
});
popupWindow = new PopupWindow(popView, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, true);
popupWindow.setOutsideTouchable(true);
popupWindow.setBackgroundDrawable(new BitmapDrawable());
if (popupWindow.isShowing()) {
popupWindow.dismiss();
}
int deleteHeight = tvDelete.getHeight() == 0 ? 280 : tvDelete.getHeight();
int deleteWidth = tvDelete.getWidth() == 0 ? 212 : tvDelete.getWidth();
popupWindow.showAsDropDown(v, (v.getWidth() - deleteWidth) / 2, v.getHeight() - deleteHeight);
}
// modify @ + }
deleteHeight和deleteWidth是在调试很多次之后确定的最佳位置。
新风格需要在长按图标时显示删除选项,同时在拖动图标时,自然就不显示了:
public void onDragOver(DragObject d) {
// modify @ + {
if (null != popupWindow && popupWindow.isShowing()) {
popupWindow.dismiss();
}
// modify @ + }
...
}
layout文件:
packages/apps/Launcher3/res/layout/layout_long_click_dialog.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white" >
<TextView
android:id="@+id/delete_target_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:padding="5dp"
android:drawableTop="@drawable/ic_launcher_trashcan_normal_holo"
android:text="@string/delete_target_uninstall_label"
android:textColor="@android:color/black" />
</LinearLayout>
3. 隐藏原生删除区域
另外,原生删除中有判断是否是系统apk,如果是则不弹删除区域,我们需要此判断,同时不显示原生的删除区域:
packages/apps/Launcher3/src/com/android/launcher3/DeleteDropTarget.java
@Override
public void onDragStart(DragSource source, Object info, int dragAction) {
boolean isVisible = true;
boolean useUninstallLabel = !LauncherAppState.isDisableAllApps() &&
isAllAppsApplication(source, info);
boolean useDeleteLabel = !useUninstallLabel && source.supportsDeleteDropTarget();
// If we are dragging an application from AppsCustomize, only show the control if we can
// delete the app (it was downloaded), and rename the string to "uninstall" in such a case.
// Hide the delete target if it is a widget from AppsCustomize.
if (!willAcceptDrop(info) || isAllAppsWidget(source, info)) {
isVisible = false;
}
if (useUninstallLabel) {
setCompoundDrawablesRelativeWithIntrinsicBounds(mUninstallDrawable, null, null, null);
} else if (useDeleteLabel) {
setCompoundDrawablesRelativeWithIntrinsicBounds(mRemoveDrawable, null, null, null);
} else {
isVisible = false;
}
mCurrentDrawable = (TransitionDrawable) getCurrentDrawable();
mActive = isVisible;
resetHoverColor();
((ViewGroup) getParent()).setVisibility(isVisible ? View.VISIBLE : View.GONE);
if (isVisible && getText().length() > 0) {
setText(useUninstallLabel ? R.string.delete_target_uninstall_label
: R.string.delete_target_label);
}
}
通过添加log发现willAcceptDrop判断是否是系统应用,当然,我们始终将isVisible设置为false,则原生的删除区域就不会显示了。
4.UI优化
删除图标变为圆角,即TextView修改为圆角,原生的TextView是四四方方的直角。
packages/apps/Launcher3/res/layout/layout_long_click_dialog.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:id="@+id/delete_target_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/textview_fillet_back"
android:drawableTop="@drawable/ic_launcher_trashcan_normal_holo"
android:gravity="center"
android:text="@string/delete_target_uninstall_label"
android:textColor="@android:color/black" />
</LinearLayout>
packages/apps/Launcher3/res/drawable/textview_fillet_back.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/white"/>
<corners android:radius="10dp"/>
<padding
android:left="5dp"
android:top="5dp"
android:right="5dp"
android:bottom="5dp" />
</shape>
5.新特性Shortcut添加删除功能
android新特性,原生Launcher3长按桌面图标会显示shortcut,如下图所示为原生设置的shortcut。
应用可以通过AndroidManifest.xml添加如下配置@xml/shortcuts,同样以原生设置为例:
<!-- Alias for launcher activity only, as this belongs to each profile. -->
<activity-alias android:name="Settings"
android:taskAffinity="com.android.settings"
android:label="@string/settings_label_launcher"
android:launchMode="singleTask"
android:targetActivity="Settings">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts"/>
</activity-alias>
shortcuts.xml如下:
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2016 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
<shortcut
android:shortcutId="manifest-shortcut-wifi"
android:icon="@drawable/ic_shortcut_wireless"
android:shortcutShortLabel="@string/wifi_settings" >
<intent android:action="android.settings.WIFI_SETTINGS" />
</shortcut>
<shortcut
android:shortcutId="manifest-shortcut-data-usage"
android:icon="@drawable/ic_shortcut_data_usage"
android:shortcutShortLabel="@string/data_usage_summary_title">
<intent
android:action="android.intent.action.MAIN"
android:targetPackage="com.android.settings"
android:targetClass="com.android.settings.Settings$DataUsageSummaryActivity" />
</shortcut>
<shortcut
android:shortcutId="manifest-shortcut-battery"
android:icon="@drawable/ic_shortcut_battery"
android:shortcutShortLabel="@string/power_usage_summary_title" >
<intent android:action="android.intent.action.POWER_USAGE_SUMMARY" />
</shortcut>
</shortcuts>
开发者可以为自己的应用添加独具特色的shortcut。
那么系统级别该如何统一添加删除的shortcut呢?
根据“应用信息”的提示,可以很快找到代码定位:
packages/apps/Launcher3/src/com/android/launcher3/popup/SystemShortcut.java
public static class AppInfo extends SystemShortcut {
public AppInfo() {
super(R.drawable.ic_info_no_shadow, R.string.app_info_drop_target_label);
}
@Override
public View.OnClickListener getOnClickListener(final Launcher launcher,
final ItemInfo itemInfo) {
return new View.OnClickListener() {
@Override
public void onClick(View view) {
Rect sourceBounds = launcher.getViewBounds(view);
Bundle opts = launcher.getActivityLaunchOptions(view);
InfoDropTarget.startDetailsActivityForInfo(itemInfo, launcher, null, sourceBounds, opts);
launcher.getUserEventDispatcher().logActionOnControl(Action.Touch.TAP,
ControlType.APPINFO_TARGET, view);
}
};
}
}
添加这个shortcut的地方:
packages/apps/Launcher3/src/com/android/launcher3/popup/PopupDataProvider.java
/** Note that these are in order of priority. */
private static final SystemShortcut[] SYSTEM_SHORTCUTS = new SystemShortcut[] {
new SystemShortcut.AppInfo(),
new SystemShortcut.Widgets(),
};
// 通过是否含有getOnClickListener方法过滤是否显示该shortcut
public @NonNull List<SystemShortcut> getEnabledSystemShortcutsForItem(ItemInfo info) {
List<SystemShortcut> systemShortcuts = new ArrayList<>();
for (SystemShortcut systemShortcut : SYSTEM_SHORTCUTS) {
if (systemShortcut.getOnClickListener(mLauncher, info) != null) {
systemShortcuts.add(systemShortcut);
}
}
return systemShortcuts;
}
实现代码patch如下:
Index: packages/apps/Launcher3/src/com/android/launcher3/popup/PopupDataProvider.java
===================================================================
--- packages/apps/Launcher3/src/com/android/launcher3/popup/PopupDataProvider.java (版本 1536)
+++ packages/apps/Launcher3/src/com/android/launcher3/popup/PopupDataProvider.java (工作副本)
@@ -52,6 +52,7 @@
/** Note that these are in order of priority. */
private static final SystemShortcut[] SYSTEM_SHORTCUTS = new SystemShortcut[] {
new SystemShortcut.AppInfo(),
+ new SystemShortcut.AppUninstall(),
new SystemShortcut.Widgets(),
};
Index: packages/apps/Launcher3/src/com/android/launcher3/popup/SystemShortcut.java
===================================================================
--- packages/apps/Launcher3/src/com/android/launcher3/popup/SystemShortcut.java (版本 1536)
+++ packages/apps/Launcher3/src/com/android/launcher3/popup/SystemShortcut.java (工作副本)
@@ -11,6 +11,7 @@
import com.android.launcher3.ItemInfo;
import com.android.launcher3.Launcher;
import com.android.launcher3.R;
+import com.android.launcher3.UninstallDropTarget;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.widget.WidgetsBottomSheet;
@@ -95,4 +96,25 @@
};
}
}
+
+ public static class AppUninstall extends SystemShortcut {
+ public AppUninstall() {
+ super(R.drawable.ic_uninstall_no_shadow, R.string.uninstall_drop_target_label);
+ }
+
+ @Override
+ public View.OnClickListener getOnClickListener(final Launcher launcher,
+ final ItemInfo itemInfo) {
+ if (!UninstallDropTarget.supportsDrop(launcher, itemInfo)) {
+ return null;
+ }
+ return new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ AbstractFloatingView.closeOpenContainer(launcher, AbstractFloatingView.TYPE_POPUP_CONTAINER_WITH_ARROW);
+ UninstallDropTarget.startUninstallActivity(launcher, itemInfo);
+ }
+ };
+ }
+ }
}
效果如下图:
此处有一个地方特别需要注意:在点击卸载后,shortcut框依旧存在,需要通过以下代码先将shortcut不显示,再进行卸载流程。
AbstractFloatingView.closeOpenContainer(launcher, AbstractFloatingView.TYPE_POPUP_CONTAINER_WITH_ARROW);