我是靠谱客的博主 会撒娇故事,最近开发中收集的这篇文章主要介绍Android原生Launcher3简要分析,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

Launcher是android手机启动后第一个看到的界面,即手机系统的桌面,下面我们就以android原生的Launcher3为例看看界面布局和显示的数据怎么获取的来简要分析下android手机桌面

Launcher中第一显示的Activity为Launcher.java,下面我们主要看看这个布局文件launcher.xml

//packages/apps/Launcher3/res/layout-land/launcher.xml
<!-- Full screen view projects under the status bar and contains the background -->
<com.android.launcher3.LauncherRootView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
android:id="@+id/launcher"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.android.launcher3.dragndrop.DragLayer
android:id="@+id/drag_layer"
android:clipChildren="false"
android:clipToPadding="false"
android:background="@drawable/workspace_bg"
android:importantForAccessibility="no"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- The workspace contains 5 screens of cells -->
<!-- DO NOT CHANGE THE ID -->
<com.android.launcher3.Workspace
android:id="@+id/workspace"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
launcher:pageIndicator="@id/page_indicator" />
<!-- DO NOT CHANGE THE ID -->
<include layout="@layout/hotseat"
android:id="@+id/hotseat"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="right"
launcher:layout_ignoreInsets="true" />
<include
android:id="@+id/drop_target_bar"
layout="@layout/drop_target_bar_vert" />
<include layout="@layout/overview_panel"
android:id="@+id/overview_panel"
android:visibility="gone" />
<com.android.launcher3.pageindicators.PageIndicatorCaretLandscape
android:id="@+id/page_indicator"
android:layout_width="@dimen/dynamic_grid_page_indicator_height"
android:layout_height="@dimen/dynamic_grid_page_indicator_height"
android:layout_gravity="bottom|left"/>
<!-- A place holder view instead of the QSB in transposed layout -->
<View
android:layout_width="0dp"
android:layout_height="10dp"
android:id="@+id/workspace_blocked_row" />
<include layout="@layout/widgets_view"
android:id="@+id/widgets_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible" />
<include layout="@layout/all_apps"
android:id="@+id/apps_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible" />
</com.android.launcher3.dragndrop.DragLayer>
</com.android.launcher3.LauncherRootView>
接着看下对应的UI显示,也可以对着上面的布局看看


这个界面显示的主要为Shortcut和Widget,其中最下面一行的几个Shortcut比较特殊,都设置了setIsHotseat属性,在具体显示的时候会根据来判断是不是显示在最下一行,另外显示的个数也在dw_phone_hotseat.xml有默认配置的,当用户还想再这个里面放入其它apk快捷方式时,会根据除去已显示的图标后剩余空间来决定是单独显示图标,还是和其它apk图标放在一个文件夹里显示。
所有的这些数据都会放入到数据库launcher.db中保存。

Hotseat为FrameLayout的子类,通过Launcher里的getDeviceProfile获取DeviceProfile对象,里面包含特定设备的一些配置文件

在InvariantDeviceProfile.java中通过getPredefinedDeviceProfiles来获取所有的device_profiles

//packages/apps/Launcher3/src/com/android/launcher3/InvariantDeviceProfile.java
ArrayList<InvariantDeviceProfile> getPredefinedDeviceProfiles(Context context) {
ArrayList<InvariantDeviceProfile> profiles = new ArrayList<>();
try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) {
final int depth = parser.getDepth();
int type;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if ((type == XmlPullParser.START_TAG) && "profile".equals(parser.getName())) {
TypedArray a = context.obtainStyledAttributes(
Xml.asAttributeSet(parser), R.styleable.InvariantDeviceProfile);
int numRows = a.getInt(R.styleable.InvariantDeviceProfile_numRows, 0);
int numColumns = a.getInt(R.styleable.InvariantDeviceProfile_numColumns, 0);
float iconSize = a.getFloat(R.styleable.InvariantDeviceProfile_iconSize, 0);
profiles.add(new InvariantDeviceProfile(
a.getString(R.styleable.InvariantDeviceProfile_name),
a.getFloat(R.styleable.InvariantDeviceProfile_minWidthDps, 0),
a.getFloat(R.styleable.InvariantDeviceProfile_minHeightDps, 0),
numRows,
numColumns,
a.getInt(R.styleable.InvariantDeviceProfile_numFolderRows, numRows),
a.getInt(R.styleable.InvariantDeviceProfile_numFolderColumns, numColumns),
a.getInt(R.styleable.InvariantDeviceProfile_minAllAppsPredictionColumns, numColumns),
iconSize,
a.getFloat(R.styleable.InvariantDeviceProfile_iconTextSize, 0),
a.getInt(R.styleable.InvariantDeviceProfile_numHotseatIcons, numColumns),
a.getFloat(R.styleable.InvariantDeviceProfile_hotseatIconSize, iconSize),
a.getResourceId(R.styleable.InvariantDeviceProfile_defaultLayoutId, 0)));
a.recycle();
}
}
} catch (IOException|XmlPullParserException e) {
throw new RuntimeException(e);
}
return profiles;
}
解析device_profiles.xml读取获得ArrayList<InvariantDeviceProfile> profiles, 最后根据手机屏幕宽高调用findClosestDeviceProfiles和invDistWeightedInterpolate获取合适的InvariantDeviceProfile,InvariantDeviceProfile里包含了每行每列允许显示的个数等配置

packages/apps/Launcher3/src/com/android/launcher3/InvariantDeviceProfile.java
ArrayList<InvariantDeviceProfile> findClosestDeviceProfiles(
final float width, final float height, ArrayList<InvariantDeviceProfile> points) {
// Sort the profiles by their closeness to the dimensions
ArrayList<InvariantDeviceProfile> pointsByNearness = points;
Collections.sort(pointsByNearness, new Comparator<InvariantDeviceProfile>() {
public int compare(InvariantDeviceProfile a, InvariantDeviceProfile b) {
return Float.compare(dist(width, height, a.minWidthDps, a.minHeightDps),
dist(width, height, b.minWidthDps, b.minHeightDps));
}
});
return pointsByNearness;
}
下面简单贴了个device_profiles.xml的配置

/packages/apps/Launcher3/res/xml/device_profiles.xml
<profiles xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3" >
<profile
launcher:name="Nexus 4"
launcher:minWidthDps="359"
launcher:minHeightDps="567"
launcher:numRows="4"
launcher:numColumns="4"
launcher:numFolderRows="4"
launcher:numFolderColumns="4"
launcher:minAllAppsPredictionColumns="4"
launcher:iconSize="60"
launcher:iconTextSize="13.0"
launcher:numHotseatIcons="5"
launcher:hotseatIconSize="56"
launcher:defaultLayoutId="@xml/default_workspace_4x4"
/>
...
</profiles>
default_workspace_4x4.xml

packages/apps/Launcher3/res/xml/default_workspace_4x4.xml
<favorites xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3">
<!-- Hotseat -->
<include launcher:workspace="@xml/dw_phone_hotseat" />
<!-- Bottom row -->
<resolve
launcher:screen="0"
launcher:x="0"
launcher:y="-1" >
<favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_EMAIL;end" />
<favorite launcher:uri="mailto:" />
</resolve>
<resolve
launcher:screen="0"
launcher:x="1"
launcher:y="-1" >
<favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_GALLERY;end" />
<favorite launcher:uri="#Intent;type=images/*;end" />
</resolve>
<resolve
launcher:screen="0"
launcher:x="3"
launcher:y="-1" >
<favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_MARKET;end" />
<favorite launcher:uri="market://details?id=com.android.launcher" />
</resolve>
</favorites>
dw_phone_hotseat.xml

packages/apps/Launcher3/res/xml/dw_phone_hotseat.xml
<?xml version="1.0" encoding="utf-8"?>
<favorites xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3">
<!-- Hotseat (We use the screen as the position of the item in the hotseat) -->
<!-- Dialer, Messaging, [All Apps], Browser, Camera -->
<resolve
launcher:container="-101"
launcher:screen="0"
launcher:x="0"
launcher:y="0" >
<favorite launcher:uri="#Intent;action=android.intent.action.DIAL;end" />
<favorite launcher:uri="tel:123" />
<favorite launcher:uri="#Intent;action=android.intent.action.CALL_BUTTON;end" />
</resolve>
<resolve
launcher:container="-101"
launcher:screen="1"
launcher:x="1"
launcher:y="0" >
<favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_MESSAGING;end" />
<favorite launcher:uri="sms:" />
<favorite launcher:uri="smsto:" />
<favorite launcher:uri="mms:" />
<favorite launcher:uri="mmsto:" />
</resolve>
<!-- All Apps -->
<resolve
launcher:container="-101"
launcher:screen="3"
launcher:x="3"
launcher:y="0" >
<favorite
launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_BROWSER;end" />
<favorite launcher:uri="http://www.example.com/" />
</resolve>
<resolve
launcher:container="-101"
launcher:screen="4"
launcher:x="4"
launcher:y="0" >
<favorite launcher:uri="#Intent;action=android.media.action.STILL_IMAGE_CAMERA;end" />
<favorite launcher:uri="#Intent;action=android.intent.action.CAMERA_BUTTON;end" />
</resolve>
</favorites>
其中Favorites和Workspaces都会保存在launcher.db里的favorites和workspaceScreens表里,上面只是默认显示的,当用户手动把某个应用放入这块时,会根据当前的剩余空间,来决定是单独显示一个icon还是和另一个图标一起显示在一个文件夹里,通过default_workspace_4x4.xml可知道当前主屏幕上默认配置显示那些apk,当然所有这样配置都会保存在数据库中,这样当用户拖拽某个apk后,都会写入到相应数据库中的,下面只是贴了下把默认配置写入数据库的逻辑

//packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java
synchronized private void loadDefaultFavoritesIfNecessary() {
SharedPreferences sp = Utilities.getPrefs(getContext());
if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
Log.d(TAG, "loading default workspace");
AppWidgetHost widgetHost = new AppWidgetHost(getContext(), Launcher.APPWIDGET_HOST_ID);
AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHost);
if (loader == null) {
loader = AutoInstallsLayout.get(getContext(),widgetHost, mOpenHelper);
}
if (loader == null) {
final Partner partner = Partner.get(getContext().getPackageManager());
if (partner != null && partner.hasDefaultLayout()) {
final Resources partnerRes = partner.getResources();
int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT,
"xml", partner.getPackageName());
if (workspaceResId != 0) {
loader = new DefaultLayoutParser(getContext(), widgetHost,
mOpenHelper, partnerRes, workspaceResId);
}
}
}
final boolean usingExternallyProvidedLayout = loader != null;
if (loader == null) {
loader = getDefaultLayoutParser(widgetHost);
}
// There might be some partially restored DB items, due to buggy restore logic in
// previous versions of launcher.
createEmptyDB();
// Populate favorites table with initial favorites
if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0)
&& usingExternallyProvidedLayout) {
// Unable to load external layout. Cleanup and load the internal layout.
createEmptyDB();
mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
getDefaultLayoutParser(widgetHost));
}
clearFlagEmptyDbCreated();
}
}
说了这么多,先来看看主界面的显示图,再来看看所有桌面apk的信息是怎么获取,调用getActivityList获取的,其中第一个参数packageName传入的是null,查询所有配置了intent Action为ACTION_MAIN,Category为CATEGORY_LAUNCHER的应用集合列表


//packages/apps/Launcher3/src/com/android/launcher3/compat/LauncherAppsCompatV16.java
public List<LauncherActivityInfoCompat> getActivityList(String packageName,
UserHandleCompat user) {
final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
mainIntent.setPackage(packageName);
List<ResolveInfo> infos = mPm.queryIntentActivities(mainIntent, 0);
List<LauncherActivityInfoCompat> list =
new ArrayList<LauncherActivityInfoCompat>(infos.size());
for (ResolveInfo info : infos) {
list.add(new LauncherActivityInfoCompatV16(mContext, info));
}
return list;
}
进入主界面后就是一个AllAppsContainerView,继承FrameLayout,在构造方法中设置的setAdapter为AllAppsGridAdapter AllAppsGridAdapter extends RecyclerView.Adapter,并且在这里调用了mAppsRecyclerView.setApps(mApps)传入了所有的apps 信息

 //packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsContainerView.java
@Override
protected void onFinishInflate() {
super.onFinishInflate();
...
// Load the all apps recycler view
mAppsRecyclerView = (AllAppsRecyclerView) findViewById(R.id.apps_list_view);
mAppsRecyclerView.setApps(mApps);
mAppsRecyclerView.setLayoutManager(mLayoutManager);
mAppsRecyclerView.setAdapter(mAdapter);
mAppsRecyclerView.setHasFixedSize(true);
mAppsRecyclerView.addOnScrollListener(mElevationController);
mAppsRecyclerView.setElevationController(mElevationController);
...
}
 在AllAppsGridAdapter调用onCreateViewHolder和onBindViewHolder来显示view

 //packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case VIEW_TYPE_SECTION_BREAK:
return new ViewHolder(new View(parent.getContext()));
case VIEW_TYPE_ICON:
/* falls through */
case VIEW_TYPE_PREDICTION_ICON: {
BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
R.layout.all_apps_icon, parent, false);
icon.setOnClickListener(mIconClickListener);
//设置每个控件的点击事件监听,每个桌面图标都是一个自定义的BubbleTextView
icon.setOnLongClickListener(mIconLongClickListener);
icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext())
.getLongPressTimeout());
icon.setOnFocusChangeListener(mIconFocusListener);
// Ensure the all apps icon height matches the workspace icons
DeviceProfile profile = mLauncher.getDeviceProfile();
Point cellSize = profile.getCellSize();
GridLayoutManager.LayoutParams lp =
(GridLayoutManager.LayoutParams) icon.getLayoutParams();
lp.height = cellSize.y;
icon.setLayoutParams(lp);
return new ViewHolder(icon);
}
case VIEW_TYPE_EMPTY_SEARCH:
return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search,
parent, false));
case VIEW_TYPE_SEARCH_MARKET:
View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market,
parent, false);
searchMarketView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mLauncher.startActivitySafely(v, mMarketSearchIntent, null);
}
});
return new ViewHolder(searchMarketView);
case VIEW_TYPE_SEARCH_DIVIDER:
return new ViewHolder(mLayoutInflater.inflate(
R.layout.all_apps_search_divider, parent, false));
case VIEW_TYPE_PREDICTION_DIVIDER:
/* falls through */
case VIEW_TYPE_SEARCH_MARKET_DIVIDER:
return new ViewHolder(mLayoutInflater.inflate(
R.layout.all_apps_divider, parent, false));
default:
throw new RuntimeException("Unexpected view type");
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
switch (holder.getItemViewType()) {
case VIEW_TYPE_ICON: {
AppInfo info = mApps.getAdapterItems().get(position).appInfo;
BubbleTextView icon = (BubbleTextView) holder.mContent;
icon.applyFromApplicationInfo(info);
icon.setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
break;
}
case VIEW_TYPE_PREDICTION_ICON: {
AppInfo info = mApps.getAdapterItems().get(position).appInfo;
BubbleTextView icon = (BubbleTextView) holder.mContent;
icon.applyFromApplicationInfo(info);
icon.setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
break;
}
case VIEW_TYPE_EMPTY_SEARCH:
TextView emptyViewText = (TextView) holder.mContent;
emptyViewText.setText(mEmptySearchMessage);
emptyViewText.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER :
Gravity.START | Gravity.CENTER_VERTICAL);
break;
case VIEW_TYPE_SEARCH_MARKET:
TextView searchView = (TextView) holder.mContent;
if (mMarketSearchIntent != null) {
searchView.setVisibility(View.VISIBLE);
} else {
searchView.setVisibility(View.GONE);
}
break;
}
if (mBindViewCallback != null) {
mBindViewCallback.onBindView(holder);
}
}
 下面再来看看AllAppsGridAdapter构造方法,里面传入了View.OnClickListener和View.OnLongClickListener


public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnClickListener
iconClickListener, View.OnLongClickListener iconLongClickListener) {
...
}
AllAppsGridAdapter是在AllAppsContainerView的构造方法中初始化的,如下:


public AllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
Resources res = context.getResources();
mLauncher = Launcher.getLauncher(context);
mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin);
mApps = new AlphabeticalAppsList(context);
mAdapter = new AllAppsGridAdapter(mLauncher, mApps, mLauncher, this);
mApps.setAdapter(mAdapter);
mLayoutManager = mAdapter.getLayoutManager();
mItemDecoration = mAdapter.getItemDecoration();
DeviceProfile grid = mLauncher.getDeviceProfile();
if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP && !grid.isVerticalBarLayout()) {
mRecyclerViewBottomPadding = 0;
setPadding(0, 0, 0, 0);
} else {
mRecyclerViewBottomPadding =
res.getDimensionPixelSize(R.dimen.all_apps_list_bottom_padding);
}
mSearchQueryBuilder = new SpannableStringBuilder();
Selection.setSelection(mSearchQueryBuilder, 0);
}
可以看到AllAppsGridAdapter的View.OnClickListener来自mLauncher即Launcher.java,View.OnLongClickListener传入的是this,即在本类的onLongClick中处理,这样即所有item的click事件都在Launcher的onClick处理

//packages/apps/Launcher3/src/com/android/launcher3/Launcher.java
public void onClick(View v) {
// Make sure that rogue clicks don't get through while allapps is launching, or after the
// view has detached (it's possible for this to happen if the view is removed mid touch).
if (v.getWindowToken() == null) {
return;
}
if (!mWorkspace.isFinishedSwitchingState()) {
return;
}
if (v instanceof Workspace) {
if (mWorkspace.isInOverviewMode()) {
showWorkspace(true);
}
return;
}
if (v instanceof CellLayout) {
if (mWorkspace.isInOverviewMode()) {
mWorkspace.snapToPageFromOverView(mWorkspace.indexOfChild(v));
showWorkspace(true);
}
return;
}
Object tag = v.getTag();
if (tag instanceof ShortcutInfo) {
onClickAppShortcut(v);
} else if (tag instanceof FolderInfo) {
if (v instanceof FolderIcon) {
onClickFolderIcon(v);
}
} else if ((FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP && v instanceof PageIndicator) ||
(v == mAllAppsButton && mAllAppsButton != null)) {
onClickAllAppsButton(v);
} else if (tag instanceof AppInfo) {//如果是apk调用startAppShortcutOrInfoActivity,启动对应Activity
startAppShortcutOrInfoActivity(v);
} else if (tag instanceof LauncherAppWidgetInfo) {
if (v instanceof PendingAppWidgetHostView) {
onClickPendingWidget((PendingAppWidgetHostView) v);
}
}
}

当点击每个应用的icon后,就会调用startAppShortcutOrInfoActivity(View v)


private void startAppShortcutOrInfoActivity(View v) {
ItemInfo item = (ItemInfo) v.getTag();
Intent intent = item.getIntent();
if (intent == null) {
throw new IllegalArgumentException("Input must have a valid intent");
}
boolean success = startActivitySafely(v, intent, item);
getUserEventDispatcher().logAppLaunch(v, intent);
if (success && v instanceof BubbleTextView) {
mWaitingForResume = (BubbleTextView) v;
mWaitingForResume.setStayPressed(true);
}
}
在这个方法里接着调用本类的 startActivitySafely,最终在这个LauncherAppsCompat.getInstance(this).startActivityForProfile


public void startActivityForProfile(ComponentName component, UserHandleCompat user,
Rect sourceBounds, Bundle opts) {
Intent launchIntent = new Intent(Intent.ACTION_MAIN);
launchIntent.addCategory(Intent.CATEGORY_LAUNCHER);
launchIntent.setComponent(component);
launchIntent.setSourceBounds(sourceBounds);
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(launchIntent, opts);
}
这样每点击一个应该icon就会启动一个对应的Activity,本开始只是想看看点击apk启动应用的流程,找到apk icon点击事件找了半天,在这顺带记录下,当然还有很多从数据库获取桌面布局信息,更新布局信息啥的在这没有提到主要是以apk信息的获取和icon单击事件来分析的,当手机里有多个应用了下面两个属性时,就会看到会让用户选择用那个桌面

<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.HOME" />
下面简单总结下桌面的显示流程
1、手机启动后会先启动桌面,在Launcher的数据库中会有每个apk显示位置的详细信息,具体是以坐标的方式。
2、通过getActivityList查询所有需要显示在桌面的apk信息,并返回一个集合。
3、主界面是一个自定义AllAppsRecyclerView继承RecyclerView,设置了AllAppsGridAdapter会根据上面list集合的size创建多少个BubbleTextView(即看到的桌面图标)。

最后

以上就是会撒娇故事为你收集整理的Android原生Launcher3简要分析的全部内容,希望文章能够帮你解决Android原生Launcher3简要分析所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(40)

评论列表共有 0 条评论

立即
投稿
返回
顶部