1.简介
- 整理下hotseat的默认数据加载流程,方便修改查看默认数据。
- 我们集成了谷歌套件,所以launcher里默认的数据被修改了
- 简单学习下hotseat控件的加载
1.1.LoaderTask.java
这个类是launcher里数据的具体加载逻辑,当然也包括hotseat,这里就简单看下hotseat相关的,也就是数据库里的favorite表
>run
public void run() {
//..
try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
List<ShortcutInfo> allShortcuts = new ArrayList<>();
try {
//
loadWorkspace(allShortcuts, memoryLogger);
//..
>loadWorkspace
private void loadWorkspace(List<ShortcutInfo> allDeepShortcuts, LoaderMemoryLogger logger) {
loadWorkspace(allDeepShortcuts, LauncherSettings.Favorites.CONTENT_URI,
null /* selection */, logger);
}
protected void loadWorkspace(
List<ShortcutInfo> allDeepShortcuts,
Uri contentUri,
String selection,
@Nullable LoaderMemoryLogger logger) {
final Context context = mApp.getContext();
final ContentResolver contentResolver = context.getContentResolver();
final PackageManagerHelper pmHelper = new PackageManagerHelper(context);
final boolean isSafeMode = pmHelper.isSafeMode();
final boolean isSdCardReady = Utilities.isBootCompleted();
final WidgetManagerHelper widgetHelper = new WidgetManagerHelper(context);
boolean clearDb = false;
if (!GridSizeMigrationTaskV2.migrateGridIfNeeded(context)) {
// Migration failed. Clear workspace.
clearDb = true;
}
if (clearDb) {
Log.d(TAG, "loadWorkspace: resetting launcher database");
LauncherSettings.Settings.call(contentResolver,
LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
}
//加载默认的数据,搜一下method哪里用到,很容易搜到下边的LauncherProvider,见2.1
LauncherSettings.Settings.call(contentResolver,
LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES);
//下边就是查询表里的数据了,代码太多不贴了。
2.LauncherProvider
2.1.call
public Bundle call(String method, final String arg, final Bundle extras) {
if (Binder.getCallingUid() != Process.myUid()) {
return null;
}
createDbIfNotExists();
>METHOD_LOAD_DEFAULT_FAVORITES
加载默认的favorite数据
case LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES: {
loadDefaultFavoritesIfNecessary();
return null;
}
>METHOD_SWITCH_DATABASE
切换数据库,这个好像是啥小型设备才用到
case LauncherSettings.Settings.METHOD_SWITCH_DATABASE: {
//已打开的和传入的数据库名字一样,啥也不干
if (TextUtils.equals(arg, mOpenHelper.getDatabaseName())) return null;
final DatabaseHelper helper = mOpenHelper;
//是否有对应的authority数据
if (extras == null || !extras.containsKey(KEY_LAYOUT_PROVIDER_AUTHORITY)) {
mProviderAuthority = null;
} else {
mProviderAuthority = extras.getString(KEY_LAYOUT_PROVIDER_AUTHORITY);
}
//用新的数据库名字打开新的dbhelper
mOpenHelper = DatabaseHelper.createDatabaseHelper(
getContext(), arg, false /* forMigration */);
helper.close();
LauncherAppState app = LauncherAppState.getInstanceNoCreate();
if (app == null) return null;
app.getModel().forceReload();
return null;
}
2.2.loadDefaultFavoritesIfNecessary
- 获取默认的favorites数据,加载优先级如下边注解,依次查找
- 集成谷歌包以后,最终用的是GmsSampleIntegration里的partner_default_layout.xml文件
- 如果没有谷歌包,默认是launcher里的res/xml/default_workspace_(6x5根据实际使用的网格找对应文件)
/**
* Loads the default workspace based on the following priority scheme:
* 1) From the app restrictions
* 2) From a package provided by play store
* 3) From a partner configuration APK, already in the system image
* 4) The default configuration for the particular device
*/
synchronized private void loadDefaultFavoritesIfNecessary() {
SharedPreferences sp = Utilities.getPrefs(getContext());
//先判断是否创建了新的数据库,是的话需要加载默认数据
if (sp.getBoolean(mOpenHelper.getKey(EMPTY_DATABASE_CREATED), false)) {
AppWidgetHost widgetHost = mOpenHelper.newLauncherWidgetHost();
//见2.3
AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHost);
if (loader == null) {
//见4.1,gms的包里没有,返回空
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();
//这里找的是partner_default_layout.xml文件
//在GmsSampleIntegration的res目录里能找到,见3.4
int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT,
"xml", partner.getPackageName());
if (workspaceResId != 0) {
loader = new DefaultLayoutParser(getContext(), widgetHost,
mOpenHelper, partnerRes, workspaceResId);
}
}
}
//是否使用的是外部的layout
final boolean usingExternallyProvidedLayout = loader != null;
if (loader == null) {
//没有外部layout,则加载launcher默认的,见2.4
loader = getDefaultLayoutParser(widgetHost);
}
//见小节 7.1,删除并创建新的favorites表
mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
//见7.4
if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0)
&& usingExternallyProvidedLayout) {
//外部layout没有数据,那么加载默认的layout数据
mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
getDefaultLayoutParser(widgetHost));
}
//清除if条件里的EMPTY_DATABASE_CREATED的值
clearFlagEmptyDbCreated();
}
}
>DatabaseHelper
上边的EMPTY_DATABASE_CREATED的值在这里被修改为true
protected void onEmptyDbCreated() {
// Set the flag for empty DB
LauncherPrefs.getPrefs(mContext).edit().putBoolean(getKey(EMPTY_DATABASE_CREATED), true)
.commit();
}
上述方法在onCreate里调用,
public void onCreate(SQLiteDatabase db) {
mMaxItemId = 1;
addFavoritesTable(db, false);
// Fresh and clean launcher DB.
mMaxItemId = initializeMaxItemId(db);
if (!mForMigration) {
//mForMigration是构造方法里赋值的
onEmptyDbCreated();
}
}
public DatabaseHelper(Context context, String dbName, boolean forMigration) {
super(context, dbName, SCHEMA_VERSION);
mContext = context;
mForMigration = forMigration;
}
查看下构造方法的创建,
- 迁移数据到时候,这个是true
- 正常是false
>createDbIfNotExists
protected synchronized void createDbIfNotExists() {
if (mOpenHelper == null) {
mOpenHelper = DatabaseHelper.createDatabaseHelper(
getContext(), false /* forMigration */);
RestoreDbTask.restoreIfNeeded(getContext(), mOpenHelper);
}
}
2.3.createWorkspaceLoaderFromAppRestriction
根据提供的authority查找,
private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction(
LauncherWidgetHolder widgetHolder) {
Context ctx = getContext();
final String authority;
if (!TextUtils.isEmpty(mProviderAuthority)) {
authority = mProviderAuthority;
} else {
authority = Settings.Secure.getString(ctx.getContentResolver(),
"launcher3.layout.provider");
}
//公司设备上边2返回都是空
if (TextUtils.isEmpty(authority)) {
return null;
}
ProviderInfo pi = ctx.getPackageManager().resolveContentProvider(authority, 0);
if (pi == null) {
return null;
}
Uri uri = getLayoutUri(authority, ctx);
try (InputStream in = ctx.getContentResolver().openInputStream(uri)) {
// Read the full xml so that we fail early in case of any IO error.
String layout = new String(IOUtils.toByteArray(in));
XmlPullParser parser = Xml.newPullParser();
parser.setInput(new StringReader(layout));
return new AutoInstallsLayout(ctx, widgetHolder, mOpenHelper,
ctx.getPackageManager().getResourcesForApplication(pi.applicationInfo),
() -> parser, AutoInstallsLayout.TAG_WORKSPACE);
} catch (Exception e) {
return null;
}
}
2.4.getDefaultLayoutParser
这个就是launcher里默认的配置
private DefaultLayoutParser getDefaultLayoutParser(LauncherWidgetHolder widgetHolder) {
InvariantDeviceProfile idp = LauncherAppState.getIDP(getContext());
//mDefaultWorkspaceLayoutOverride是测试用的,正常用的是idp里的
int defaultLayout = mDefaultWorkspaceLayoutOverride > 0
? mDefaultWorkspaceLayoutOverride : idp.defaultLayoutId;
return new DefaultLayoutParser(getContext(), widgetHolder,
mOpenHelper, getContext().getResources(), defaultLayout);
}
idp里的数据来源是xml文件,位置:res/xml/device_profiles.xml,不同的网格数据不同,如下
<grid-option
launcher:name="6_by_5"
launcher:devicePaddingId="@xml/paddings_6x5"
launcher:dbFile="launcher_6_by_5.db"
launcher:defaultLayoutId="@xml/default_workspace_6x5"
如果没有集成GMS包,那么AOSP里launcher的默认数据来源就是default_workspace_6x5.xml了(后边的6x5根据实际尺寸来),打开后可以看到favorites标签
>default_workspace_6x5.xml
<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) -->
<!-- Mail Calendar Gallery Store Internet Camera -->
<resolve
launcher:container="-101"
launcher:screen="0"
launcher:x="0"
launcher:y="0" >
<favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_EMAIL;end" />
<favorite launcher:uri="mailto:" />
</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_CALENDAR;end" />
</resolve>
//...
3.Partner
public static final String RES_PARTNER_DEFAULT_LAYOUT = "partner_default_layout";
3.1.get
private static final String
ACTION_PARTNER_CUSTOMIZATION = "com.android.launcher3.action.PARTNER_CUSTOMIZATION";
public static Partner get(PackageManager pm) {
return get(pm, ACTION_PARTNER_CUSTOMIZATION);
}
public static Partner get(PackageManager pm, String action) {
Pair<String, Resources> apkInfo = findSystemApk(action, pm);
return apkInfo != null ? new Partner(apkInfo.first, apkInfo.second) : null;
}
>findSystemApk
找到符合action的广播接收者
private static Pair<String, Resources> findSystemApk(String action, PackageManager pm) {
final Intent intent = new Intent(action);
for (ResolveInfo info : pm.queryBroadcastReceivers(intent, MATCH_SYSTEM_ONLY)) {
final String packageName = info.activityInfo.packageName;
try {
final Resources res = pm.getResourcesForApplication(packageName);
return Pair.create(packageName, res);
}
return null;
}
3.2.ACTION_PARTNER_CUSTOMIZATION
查找符合要求的receiver
>空实现的
vendor/partner_gms/apps/GmsSampleIntegration/AndroidManifest.xml
<!-- This isn't a real receiver, it's only used as a marker interface. -->
<receiver android:name=".LauncherCustomizationReceiver"
android:exported="true">
<intent-filter>
<action android:name="com.android.launcher3.action.PARTNER_CUSTOMIZATION" />
</intent-filter>
</receiver>
3.3.getXmlResId
public int getXmlResId(String layoutName) {
return getResources().getIdentifier(layoutName, "xml", getPackageName());
}
3.4.partner_default_layout.xml
可以看到,gms包里,默认的hotseat数据有5个(container是-101的),后边还有个文件夹里边包了一堆谷歌应用
<favorites>
<!-- Hotseat (We use the screen as the position of the item in the hotseat) -->
<!-- Dialer Messaging Calendar Contacts Camera -->
<favorite container="-101" screen="0" x="0" y="0" packageName="com.google.android.dialer" className="com.google.android.dialer.extensions.GoogleDialtactsActivity"/>
<favorite container="-101" screen="1" x="1" y="0" packageName="com.google.android.apps.messaging" className="com.google.android.apps.messaging.ui.ConversationListActivity"/>
<favorite container="-101" screen="2" x="2" y="0" packageName="com.google.android.calendar" className="com.android.calendar.AllInOneActivity"/>
<favorite container="-101" screen="3" x="3" y="0" packageName="com.google.android.contacts" className="com.android.contacts.activities.PeopleActivity"/>
<favorite container="-101" screen="4" x="4" y="0" packageName="com.android.camera2" className="com.android.camera.CameraActivity"/>
<folder title="@string/google_folder_title" screen="0" x="0" y="4">
<favorite packageName="com.google.android.googlequicksearchbox" className="com.google.android.googlequicksearchbox.SearchActivity"/>
<favorite packageName="com.android.chrome" className="com.google.android.apps.chrome.Main"/>
<favorite packageName="com.google.android.gm" className="com.google.android.gm.ConversationListActivityGmail"/>
<favorite packageName="com.google.android.apps.maps" className="com.google.android.maps.MapsActivity"/>
<favorite packageName="com.google.android.youtube" className="com.google.android.youtube.app.honeycomb.Shell$HomeActivity"/>
<favorite packageName="com.google.android.apps.docs" className="com.google.android.apps.docs.app.NewMainProxyActivity"/>
<favorite packageName="com.google.android.apps.youtube.music" className="com.google.android.apps.youtube.music.activities.MusicActivity"/>
<favorite packageName="com.google.android.videos" className="com.google.android.videos.GoogleTvEntryPoint"/>
<favorite packageName="com.google.android.apps.tachyon" className="com.google.android.apps.tachyon.MainActivity"/>
<favorite packageName="com.google.android.apps.photos" className="com.google.android.apps.photos.home.HomeActivity"/>
</folder>
4.AutoInstallsLayout
4.1.get
参看3.2对应的谷歌包,在里边没找到default_layout开头的xml文件,所以这个返回的是空
private static final String FORMATTED_LAYOUT_RES_WITH_HOSTEAT = "default_layout_%dx%d_h%s";
private static final String FORMATTED_LAYOUT_RES = "default_layout_%dx%d";
private static final String LAYOUT_RES = "default_layout";
static AutoInstallsLayout get(Context context, LauncherWidgetHolder appWidgetHolder,
LayoutParserCallback callback) {
//这个参考小节 3.1
Partner partner = Partner.get(context.getPackageManager(), ACTION_LAUNCHER_CUSTOMIZATION);
if (partner == null) {
return null;
}
InvariantDeviceProfile grid = LauncherAppState.getIDP(context);
// 根据网格的行列以及hotseat的个数,获取布局名字,比如default_layout_6x5_h4
String layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES_WITH_HOSTEAT,
grid.numColumns, grid.numRows, grid.numDatabaseHotseatIcons);
//看下有没有对应的文件
int layoutId = partner.getXmlResId(layoutName);
// Try with only grid size
if (layoutId == 0) {
//上边的 没找到,这次找default_layout_6x5
layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES,
grid.numColumns, grid.numRows);
layoutId = partner.getXmlResId(layoutName);
}
// Try the default layout
if (layoutId == 0) {
//还是没找到,这次找default_layout.xml
layoutId = partner.getXmlResId(LAYOUT_RES);
}
if (layoutId == 0) {
//还没找到,那就是没有
return null;
}
return new AutoInstallsLayout(context, appWidgetHolder, callback, partner.getResources(),
layoutId, TAG_WORKSPACE);
}
5.Launcher
上篇里简单看了下launcher数据的获取流程,获取数据以后会有各种回调的,这里看下launcher里的回调
5.1.bindItems
public void bindItems(
final List<ItemInfo> items,//这个就是获取到的数据集合
final boolean forceAnimateIcons,
final boolean focusFirstItemForAccessibility) {
// Get the list of added items and intersect them with the set of items here
final Collection<Animator> bounceAnims = new ArrayList<>();
boolean canAnimatePageChange = canAnimatePageChange();
Workspace<?> workspace = mWorkspace;
int newItemsScreenId = -1;
int end = items.size();
View newView = null;
for (int i = 0; i < end; i++) {
final ItemInfo item = items.get(i);
//..
final View view;//根据itemType创建不同类型的view
switch (item.itemType) {
//..
workspace.addInScreenFromBind(view, item);//添加到容器里,见6.1
if (newView == null) {
newView = view;
}
}
6.WorkspaceLayoutManager.java
Workspace实现了这个接口,所以上边的workspace调用的就是这个接口里的方法
6.1.addInScreenFromBind
default void addInScreenFromBind(View child, ItemInfo info) {
int x = info.cellX;
int y = info.cellY;
if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
|| info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
int screenId = info.screenId;
x = getHotseat().getCellXFromOrder(screenId);
y = getHotseat().getCellYFromOrder(screenId);
}
addInScreen(child, info.container, info.screenId, x, y, info.spanX, info.spanY);
}
>addInScreen
default void addInScreen(View child, int container, int screenId, int x, int y,
int spanX, int spanY) {
final CellLayout layout;
if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
|| container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
//容器类型是hotseat,获取对应容器
layout = getHotseat();
//hotseat只显示图标,不显示文字
if (child instanceof FolderIcon) {
((FolderIcon) child).setTextVisible(false);
}
} else {
// Show folder title if not in the hotseat
if (child instanceof FolderIcon) {
((FolderIcon) child).setTextVisible(true);
}
//这个是workspace了,可以有多个屏,根据当前id获取对应的容器
layout = getScreenWithId(screenId);
}
ViewGroup.LayoutParams genericLp = child.getLayoutParams();
CellLayoutLayoutParams lp;
//设置layoutParams
if (genericLp == null || !(genericLp instanceof CellLayoutLayoutParams)) {
lp = new CellLayoutLayoutParams(x, y, spanX, spanY, screenId);
} else {
lp = (CellLayoutLayoutParams) genericLp;
lp.cellX = x;
lp.cellY = y;
lp.cellHSpan = spanX;
lp.cellVSpan = spanY;
}
if (spanX < 0 && spanY < 0) {
lp.isLockedToGrid = false;
}
// Get the canonical child id to uniquely represent this view in this screen
ItemInfo info = (ItemInfo) child.getTag();
int childId = info.getViewId();
boolean markCellsAsOccupied = !(child instanceof Folder);
//添加到对应的容器里
if (!layout.addViewToCellLayout(child, -1, childId, lp, markCellsAsOccupied)) {
}
child.setHapticFeedbackEnabled(false);
child.setOnLongClickListener(getWorkspaceChildOnLongClickListener());
if (child instanceof DropTarget) {
onAddDropTarget((DropTarget) child);
}
}
7.DatabaseHelper
7.1.createEmptyDB
清除favorites表和workspaceScreens表,并创建新的表
/**
* Clears all the data for a fresh start.
*/
public void createEmptyDB(SQLiteDatabase db) {
try (SQLiteTransaction t = new SQLiteTransaction(db)) {
dropTable(db, Favorites.TABLE_NAME);
dropTable(db, "workspaceScreens");
onCreate(db);
t.commit();
}
}
7.2.onCreate
public void onCreate(SQLiteDatabase db) {
mMaxItemId = 1;
addFavoritesTable(db, false);//添加favorite表
// Fresh and clean launcher DB.
mMaxItemId = initializeMaxItemId(db);
if (!mForMigration) {
onEmptyDbCreated();
}
}
>onEmptyDbCreated
修改sp的值,表明默认的db已创建
protected void onEmptyDbCreated() {
// Set the flag for empty DB
LauncherPrefs.getPrefs(mContext).edit().putBoolean(getKey(EMPTY_DATABASE_CREATED), true)
.commit();
}
7.3.createDatabaseHelper
static DatabaseHelper createDatabaseHelper(Context context, boolean forMigration) {
return createDatabaseHelper(context, null, forMigration);
}
static DatabaseHelper createDatabaseHelper(Context context, String dbName,
boolean forMigration) {
if (dbName == null) {
//默认的db名字,可以参考2.4里读取的配置文件
dbName = InvariantDeviceProfile.INSTANCE.get(context).dbFile;
}
DatabaseHelper databaseHelper = new DatabaseHelper(context, dbName, forMigration);
//如果favorites表没有创建,默认创建
if (!tableExists(databaseHelper.getReadableDatabase(), Favorites.TABLE_NAME)) {
databaseHelper.addFavoritesTable(databaseHelper.getWritableDatabase(), true);
}
//hotseat_restore_backup是否存在
databaseHelper.mHotseatRestoreTableExists = tableExists(
databaseHelper.getReadableDatabase(), Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);
databaseHelper.initIds();//获取favorites表的最大id
return databaseHelper;
}
7.4.loadFavorites
@Thunk int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) {
//见8.1
int count = loader.loadLayout(db, new IntArray());
//获取表的最大id值
mMaxItemId = initializeMaxItemId(db);
return count;
}
>initializeMaxItemId
就是查找数据库,获取id的最大值
private int initializeMaxItemId(SQLiteDatabase db) {
return getMaxId(db, "SELECT MAX(%1$s) FROM %2$s", Favorites._ID, Favorites.TABLE_NAME);
}
7.5.newLauncherWidgetHost
public LauncherWidgetHolder newLauncherWidgetHolder() {
return LauncherWidgetHolder.newInstance(mContext);
}
通过factory实例化的,在配置里配置的
public final class QuickstepWidgetHolder extends LauncherWidgetHolder {
7.6.insertAndCheck
public int insertAndCheck(SQLiteDatabase db, ContentValues values) {
return dbInsertAndCheck(this, db, Favorites.TABLE_NAME, null, values);
}
>dbInsertAndCheck
@Thunk static int dbInsertAndCheck(DatabaseHelper helper,
SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) {
if (values == null) {
throw new RuntimeException("Error: attempting to insert null values");
}
if (!values.containsKey(LauncherSettings.Favorites._ID)) {
throw new RuntimeException("Error: attempting to add item without specifying an id");
}
helper.checkId(values);
return (int) db.insert(table, nullColumnHack, values);
}
8.DefaultLayoutParser
public class DefaultLayoutParser extends AutoInstallsLayout {
8.1.loadLayout
父类实现
public int loadLayout(SQLiteDatabase db, IntArray screenIds) {
mDb = db;
try {
return parseLayout(mInitialLayoutSupplier.get(), screenIds);
} catch (Exception e) {
return -1;
}
}
8.2.parseLayout
父类实现
/**
* Parses the layout and returns the number of elements added on the homescreen.
*/
protected int parseLayout(XmlPullParser parser, IntArray screenIds)
throws XmlPullParserException, IOException {
beginDocument(parser, mRootTag);
final int depth = parser.getDepth();
int type;
ArrayMap<String, TagParser> tagParserMap = getLayoutElementsMap();
int count = 0;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
count += parseAndAddNode(parser, tagParserMap, screenIds);
}
return count;
}
>getLayoutElementsMap
map里存储了支持的tag,以及对应tag用到的解析器
protected ArrayMap<String, TagParser> getLayoutElementsMap() {
ArrayMap<String, TagParser> parsers = new ArrayMap<>();
parsers.put(TAG_FAVORITE, new AppShortcutWithUriParser());
parsers.put(TAG_APPWIDGET, new AppWidgetParser());
parsers.put(TAG_SEARCH_WIDGET, new SearchWidgetParser());
parsers.put(TAG_SHORTCUT, new UriShortcutParser(mSourceRes));
parsers.put(TAG_RESOLVE, new ResolveParser());
parsers.put(TAG_FOLDER, new MyFolderParser());
parsers.put(TAG_PARTNER_FOLDER, new PartnerFolderParser());
return parsers;
}
8.3.parseAndAddNode
父类实现
protected int parseAndAddNode(
XmlPullParser parser, ArrayMap<String, TagParser> tagParserMap, IntArray screenIds)
throws XmlPullParserException, IOException {
if (TAG_INCLUDE.equals(parser.getName())) {
//include标签,说明可以引用其他文件里的数据
final int resId = getAttributeResourceValue(parser, ATTR_WORKSPACE, 0);
if (resId != 0) {
// recursively load some more favorites, why not?
return parseLayout(mSourceRes.getXml(resId), screenIds);
} else {
return 0;
}
}
mValues.clear();
//见8.4,获取容器id以及屏幕id
parseContainerAndScreen(parser, mTemp);
final int container = mTemp[0];
final int screenId = mTemp[1];
mValues.put(Favorites.CONTAINER, container);
mValues.put(Favorites.SCREEN, screenId);
mValues.put(Favorites.CELLX,
convertToDistanceFromEnd(getAttributeValue(parser, ATTR_X), mColumnCount));
mValues.put(Favorites.CELLY,
convertToDistanceFromEnd(getAttributeValue(parser, ATTR_Y), mRowCount));
//根据tag获取对应的解析器
TagParser tagParser = tagParserMap.get(parser.getName());
if (tagParser == null) {
//解析器为null,返回
return 0;
}
//调用对应解析器解析
int newElementId = tagParser.parseAndAdd(parser);
if (newElementId >= 0) {
// Keep track of the set of screens which need to be added to the db.
if (!screenIds.contains(screenId) &&
container == Favorites.CONTAINER_DESKTOP) {
screenIds.add(screenId);
}
return 1;
}
return 0;
}
>convertToDistanceFromEnd
负的表示从右往左数,正的从左往右,为空表示就在末尾
private static String convertToDistanceFromEnd(String value, int endValue) {
if (!TextUtils.isEmpty(value)) {
int x = Integer.parseInt(value);
if (x < 0) {
return Integer.toString(endValue + x);
}
}
return value;
}
8.4.parseContainerAndScreen
protected void parseContainerAndScreen(XmlPullParser parser, int[] out) {
//默认container是-100
out[0] = LauncherSettings.Favorites.CONTAINER_DESKTOP;
String strContainer = getAttributeValue(parser, ATTR_CONTAINER);
if (strContainer != null) {
//有设置container,就用设置的
out[0] = Integer.parseInt(strContainer);
}
out[1] = Integer.parseInt(getAttributeValue(parser, ATTR_SCREEN));
}
9.AppShortcutParser
AutoInstallsLayout.java的内部类,有各种各样的解析器,我们研究一种就行了。
protected class AppShortcutParser implements TagParser {
9.1.parseAndAdd
public int parseAndAdd(XmlPullParser parser) {
final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME);
final String className = getAttributeValue(parser, ATTR_CLASS_NAME);
//参考3.4 有包名和类名的,走if
//参考2.4,没有包名和类名的,走else,另外一种解析方法,具体实现在子类。
if (!TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(className)) {
ActivityInfo info;
try {
ComponentName cn;
try {
cn = new ComponentName(packageName, className);
info = mPackageManager.getActivityInfo(cn, 0);
} catch (PackageManager.NameNotFoundException nnfe) {
String[] packages = mPackageManager.currentToCanonicalPackageNames(
new String[]{packageName});
cn = new ComponentName(packages[0], className);
info = mPackageManager.getActivityInfo(cn, 0);
}
final Intent intent = new Intent(Intent.ACTION_MAIN, null)
.addCategory(Intent.CATEGORY_LAUNCHER)
.setComponent(cn)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
return addShortcut(info.loadLabel(mPackageManager).toString(),
intent, Favorites.ITEM_TYPE_APPLICATION);
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Favorite not found: " + packageName + "/" + className);
}
return -1;
} else {
return invalidPackageOrClass(parser);
}
}
>addShortcut
这里的callback是构造方法里传过来的,就是小节7的databaseHelper
protected int addShortcut(String title, Intent intent, int type) {
int id = mCallback.generateNewItemId();
mValues.put(Favorites.INTENT, intent.toUri(0));
mValues.put(Favorites.TITLE, title);
mValues.put(Favorites.ITEM_TYPE, type);
mValues.put(Favorites.SPANX, 1);
mValues.put(Favorites.SPANY, 1);
mValues.put(Favorites._ID, id);
//见7.6
if (mCallback.insertAndCheck(mDb, mValues) < 0) {
return -1;
} else {
return id;
}
}
10.HotseatEduDialog
//父类见小节11,可以看到是个线性布局
public class HotseatEduDialog extends AbstractSlideInView<Launcher> implements Insettable {
10.1.predicted_hotseat_edu.xml
10.7里用这个布局
<com.android.launcher3.hybridhotseat.HotseatEduDialog xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_gravity="bottom"
android:layout_height="wrap_content"
android:gravity="bottom"
android:orientation="vertical">
<!--顶部固定的padding,不带背景的-->
<View
android:layout_width="match_parent"
android:layout_height="32dp"
android:backgroundTint="?attr/eduHalfSheetBGColor"
android:background="@drawable/bottom_sheet_top_border" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/eduHalfSheetBGColor" //背景颜色
android:orientation="vertical">
<!--图上第一行文字-->
<TextView
style="@style/TextHeadline"
android:id="@+id/hotseat_edu_heading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="18dp"
android:paddingLeft="@dimen/bottom_sheet_edu_padding"
android:paddingRight="@dimen/bottom_sheet_edu_padding"
android:text="@string/hotseat_edu_title_migrate"
android:fontFamily="google-sans"
android:textAlignment="center"
android:textColor="@android:color/white"
android:textSize="20sp" />
<!--图上第二行文字-->
<TextView
android:layout_width="match_parent"
android:id="@+id/hotseat_edu_content"
android:layout_height="wrap_content"
android:layout_marginTop="18dp"
android:layout_marginBottom="18dp"
android:fontFamily="roboto-medium"
android:paddingLeft="@dimen/bottom_sheet_edu_padding"
android:paddingRight="@dimen/bottom_sheet_edu_padding"
android:text="@string/hotseat_edu_message_migrate"
android:textAlignment="center"
android:textColor="@android:color/white"
android:textSize="16sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/hotseat_wrapper"
android:orientation="vertical">
<!--这个就是显示图标的容器-->
<com.android.launcher3.CellLayout
android:id="@+id/sample_prediction"
android:layout_width="match_parent"
android:layout_height="0dp"
launcher:containerType="hotseat" />
<LinearLayout
android:id="@+id/button_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/bottom_sheet_edu_padding"
android:paddingTop="8dp"
android:paddingRight="@dimen/bottom_sheet_edu_padding">
<FrameLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight=".4">
<Button
android:id="@+id/no_thanks"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:text="@string/hotseat_edu_dismiss"
android:layout_gravity="start|center_vertical"
android:textColor="@android:color/white"/>
</FrameLayout>
<!--右侧按钮,右侧间距在10.3里动态调整过-->
<FrameLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight=".6">
<Button
android:id="@+id/turn_predictions_on"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:layout_gravity="end|center_vertical"
android:text="@string/hotseat_edu_accept"
android:textColor="@android:color/white"/>
</FrameLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</com.android.launcher3.hybridhotseat.HotseatEduDialog>
10.2.onLayout
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
//可以看到布局完整以后默认是close状态,见11.3
setTranslationShift(TRANSLATION_SHIFT_CLOSED);
}
10.3.onFinishInflate
protected void onFinishInflate() {
super.onFinishInflate();
//文本下边的容器
mHotseatWrapper = findViewById(R.id.hotseat_wrapper);
//显示图标的cellLayout
mSampleHotseat = findViewById(R.id.sample_prediction);
Context context = getContext();
DeviceProfile grid = mActivityContext.getDeviceProfile();
Rect padding = grid.getHotseatLayoutPadding(context);
//设置高度,大小
mSampleHotseat.getLayoutParams().height = grid.cellHeightPx;
mSampleHotseat.setGridSize(grid.numShownHotseatIcons, 1);
mSampleHotseat.setPadding(padding.left, 0, padding.right, 0);
//右边按钮
Button turnOnBtn = findViewById(R.id.turn_predictions_on);
turnOnBtn.setOnClickListener(this::onAccept);//补充1
//左边按钮
mDismissBtn = findViewById(R.id.no_thanks);
mDismissBtn.setOnClickListener(this::onDismiss);//补充2
//2个按钮所在的容器
LinearLayout buttonContainer = findViewById(R.id.button_container);
//hotseatBarEndOffset主要就是偏移了3个导航按钮的距离,包括间距
int adjustedMarginEnd = grid.hotseatBarEndOffset - buttonContainer.getPaddingEnd();
if (InvariantDeviceProfile.INSTANCE.get(context)
.getDeviceProfile(context).isTaskbarPresent && adjustedMarginEnd > 0) {
//调整margin
((LinearLayout.LayoutParams) buttonContainer.getLayoutParams()).setMarginEnd(
adjustedMarginEnd);
}
}
>1.onAccept
接受建议按钮的点击事件
private void onAccept(View v) {
mHotseatEduController.migrate();//13.1数据迁移
handleClose(true);
mHotseatEduController.moveHotseatItems();//13.2重新绑定数据
mHotseatEduController.finishOnboarding();
}
>2.onDismiss
不需要建议的按钮点击事件
private void onDismiss(View v) {
mHotseatEduController.showDimissTip();//13.3提示
mHotseatEduController.finishOnboarding();
handleClose(true);
}
10.4.isOfType
判断是否是给定的类型
protected boolean isOfType(int type) {
return (type & TYPE_ON_BOARD_POPUP) != 0;
}
10.5.setInsets
public void setInsets(Rect insets) {
int leftInset = insets.left - mInsets.left;
int rightInset = insets.right - mInsets.right;
int bottomInset = insets.bottom - mInsets.bottom;
mInsets.set(insets);
if (mActivityContext.getOrientation() == Configuration.ORIENTATION_PORTRAIT) {
setPadding(leftInset, getPaddingTop(), rightInset, 0);
mHotseatWrapper.setPadding(mHotseatWrapper.getPaddingLeft(), getPaddingTop(),
mHotseatWrapper.getPaddingRight(), bottomInset);
//垂直方向,底部容器高度是固定的了
mHotseatWrapper.getLayoutParams().height =
mActivityContext.getDeviceProfile().hotseatBarSizePx + insets.bottom;
} else {
setPadding(0, getPaddingTop(), 0, 0);
mHotseatWrapper.setPadding(mHotseatWrapper.getPaddingLeft(), getPaddingTop(),
mHotseatWrapper.getPaddingRight(),
(int) getResources().getDimension(R.dimen.bottom_sheet_edu_padding));
//横屏下文字提示有点变化
((TextView) findViewById(R.id.hotseat_edu_heading)).setText(
R.string.hotseat_edu_title_migrate_landscape);
((TextView) findViewById(R.id.hotseat_edu_content)).setText(
R.string.hotseat_edu_message_migrate_landscape);
}
}
10.6.show
public void show(List<WorkspaceItemInfo> predictions) {
if (getParent() != null //parent不为空说明已经显示了
|| predictions.size() < mActivityContext.getDeviceProfile().numShownHotseatIcons//提供的数据不足
|| mHotseatEduController == null) {//controller还没设置
return;
}
//见12.5,关闭所有打开的悬浮view
AbstractFloatingView.closeAllOpenViews(mActivityContext);
//见11.2,把自己添加到容器里
attachToContainer();
animateOpen();//补充1,带动画打开
populatePreview(predictions);//补充2,添加图标
}
>1.animateOpen
private void animateOpen() {
if (mIsOpen || mOpenCloseAnimator.isRunning()) {
//已经打开或者正在打开
return;
}
mIsOpen = true;
//动画,和close逻辑一样,这里反过来,参数为TRANSLATION_SHIFT_OPENED
mOpenCloseAnimator.setValues(
PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
mOpenCloseAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
mOpenCloseAnimator.start();
}
>2.populatePreview
给容器添加数据
private void populatePreview(List<WorkspaceItemInfo> predictions) {
for (int i = 0; i < mActivityContext.getDeviceProfile().numShownHotseatIcons; i++) {
WorkspaceItemInfo info = predictions.get(i);
PredictedAppIcon icon = PredictedAppIcon.createIcon(mSampleHotseat, info);
icon.setEnabled(false);
icon.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
icon.verifyHighRes();
CellLayoutLayoutParams lp = new CellLayoutLayoutParams(i, 0, 1, 1, -1);
mSampleHotseat.addViewToCellLayout(icon, i, info.getViewId(), lp, true);
}
}
10.7.getDialog
加载布局
public static HotseatEduDialog getDialog(Launcher launcher) {
LayoutInflater layoutInflater = LayoutInflater.from(launcher);
return (HotseatEduDialog) layoutInflater.inflate(
R.layout.predicted_hotseat_edu, launcher.getDragLayer(),
false);
}
11.AbstractSlideInView
父类见小节12
public abstract class AbstractSlideInView<T extends Context & ActivityContext>
extends AbstractFloatingView implements SingleAxisSwipeDetector.Listener {
11.1.构造方法
public AbstractSlideInView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mActivityContext = ActivityContext.lookupContext(context);
mScrollInterpolator = Interpolators.SCROLL_CUBIC;
mSwipeDetector = new SingleAxisSwipeDetector(context, this,
SingleAxisSwipeDetector.VERTICAL);
mOpenCloseAnimator = ObjectAnimator.ofPropertyValuesHolder(this);
mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mSwipeDetector.finishedScrolling();
announceAccessibilityChanges();
}
});
int scrimColor = getScrimColor(context);
mColorScrim = scrimColor != -1 ? createColorScrim(context, scrimColor) : null;
}
>1.createColorScrim
背景蒙版view
protected View createColorScrim(Context context, int bgColor) {
View view = new View(context);
view.forceHasOverlappingRendering(false);
view.setBackgroundColor(bgColor);
BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams(MATCH_PARENT, MATCH_PARENT);
lp.ignoreInsets = true;
view.setLayoutParams(lp);
return view;
}
11.2.attachToContainer
protected void attachToContainer() {
if (mColorScrim != null) {
getPopupContainer().addView(mColorScrim);
}
getPopupContainer().addView(this);
}
>1.getPopupContainer
获取容器
protected BaseDragLayer getPopupContainer() {
return mActivityContext.getDragLayer();
}
11.3.setTranslationShift
- translationShift就是移动的比例
protected void setTranslationShift(float translationShift) {
mTranslationShift = translationShift;
mContent.setTranslationY(mTranslationShift * getShiftRange());
if (mColorScrim != null) {
mColorScrim.setAlpha(1 - mTranslationShift);
}
}
>1.getShiftRange
移动的范围也就是容器的高度
protected float getShiftRange() {
return mContent.getHeight();
}
>2.TRANSLATION_SHIFT_
打开关闭状态的移动比例
protected static final float TRANSLATION_SHIFT_CLOSED = 1f;
protected static final float TRANSLATION_SHIFT_OPENED = 0f;
11.4.handleClose
protected void handleClose(boolean animate, long defaultDuration) {
if (!mIsOpen) {
return;
}
Optional.ofNullable(mOnCloseBeginListener).ifPresent(OnCloseListener::onSlideInViewClosed);
if (!animate) {//不需要动画
mOpenCloseAnimator.cancel();
//见11.3,移动y到屏幕外
setTranslationShift(TRANSLATION_SHIFT_CLOSED);
onCloseComplete();
return;
}
//需要动画,通过TRANSLATION_SHIFT处理,见补充2
mOpenCloseAnimator.setValues(
PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_CLOSED));
mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
//动画结束处理
mOpenCloseAnimator.removeListener(this);
onCloseComplete();
}
});
if (mSwipeDetector.isIdleState()) {
mOpenCloseAnimator
.setDuration(defaultDuration)
.setInterpolator(getIdleInterpolator());
} else {
mOpenCloseAnimator.setInterpolator(mScrollInterpolator);
}
//开启动画
mOpenCloseAnimator.start();
}
>1.onCloseComplete
视图关闭完成以后的处理
protected void onCloseComplete() {
mIsOpen = false;//修改标记
getPopupContainer().removeView(this);//移除view
if (mColorScrim != null) {
getPopupContainer().removeView(mColorScrim);
}
//有监听的话回调监听
mOnCloseListeners.forEach(OnCloseListener::onSlideInViewClosed);
}
>2.TRANSLATION_SHIFT
自定义的移动属性,
protected static final Property<AbstractSlideInView, Float> TRANSLATION_SHIFT =
new Property<AbstractSlideInView, Float>(Float.class, "translationShift") {
@Override
public Float get(AbstractSlideInView view) {
return view.mTranslationShift;
}
@Override
public void set(AbstractSlideInView view, Float value) {
view.setTranslationShift(value);//见11.3
}
};
12.AbstractFloatingView
public abstract class AbstractFloatingView extends LinearLayout implements TouchController {
12.1.close
public final void close(boolean animate) {
animate &= areAnimatorsEnabled();
if (mIsOpen) {
// Add to WW logging
}
//子类实现,最终见11.4
handleClose(animate);
mIsOpen = false;
}
12.2.onBackPressed
后退的时候关闭
public boolean onBackPressed() {
close(true);
return true;
}
12.3.getView
根据提供的type查找对应的控件
private static <T extends AbstractFloatingView> T getView(
ActivityContext activity, @FloatingViewType int type, boolean mustBeOpen) {
//获取容器
BaseDragLayer dragLayer = activity.getDragLayer();
if (dragLayer == null) return null;
//循环所有的child
for (int i = dragLayer.getChildCount() - 1; i >= 0; i--) {
View child = dragLayer.getChildAt(i);
//类型判断
if (child instanceof AbstractFloatingView) {
AbstractFloatingView view = (AbstractFloatingView) child;
//是否是指定的类型,必须打开为false或者是打开状态
if (view.isOfType(type) && (!mustBeOpen || view.isOpen())) {
return (T) view;
}
}
}
return null;
}
12.4.closeOpenViews
关闭type对应的views
public static void closeOpenViews(ActivityContext activity, boolean animate,
@FloatingViewType int type) {
BaseDragLayer dragLayer = activity.getDragLayer();
// Iterate in reverse order. AbstractFloatingView is added later to the dragLayer,
// and will be one of the last views.
for (int i = dragLayer.getChildCount() - 1; i >= 0; i--) {
View child = dragLayer.getChildAt(i);
//循环所有child
if (child instanceof AbstractFloatingView) {
AbstractFloatingView abs = (AbstractFloatingView) child;
//看是否是对应的type,是的话关闭child
if (abs.isOfType(type)) {
abs.close(animate);
}
}
}
}
12.5.closeAllOpenViews
public static void closeAllOpenViews(ActivityContext activity) {
closeAllOpenViews(activity, true);
}
public static void closeAllOpenViews(ActivityContext activity, boolean animate) {
closeOpenViews(activity, animate, TYPE_ALL);//见12.4
activity.finishAutoCancelActionMode();
}
13.HotseatEduController.java
13.1.migrate
void migrate() {
HotseatRestoreHelper.createBackup(mLauncher);
migrateHotseatWhole();//补充1
//已启动应用建议的提示
Snackbar.show(mLauncher, R.string.hotsaet_tip_prediction_enabled,
R.string.hotseat_prediction_settings, null,
//设置按钮的点击事件,intent见补充2
() -> mLauncher.startActivity(getSettingsIntent()));
}
>1.migrateHotseatWhole
- 首先需要知道,hotseat里有两种图标,一种是固定的pinned,一种是推荐的predicted
private int migrateHotseatWhole() {
Workspace<?> workspace = mLauncher.getWorkspace();
int pageId = -1;
int toRow = 0;
for (int i = 0; i < workspace.getPageCount(); i++) {
CellLayout target = workspace.getScreenWithId(workspace.getScreenIdForPageIndex(i));
if (target.makeSpaceForHotseatMigration(true)) {
toRow = mLauncher.getDeviceProfile().inv.numRows - 1;
pageId = i;
break;
}
}
if (pageId == -1) {
pageId = LauncherSettings.Settings.call(mLauncher.getContentResolver(),
LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
.getInt(LauncherSettings.Settings.EXTRA_VALUE);
mNewScreens = IntArray.wrap(pageId);
}
//横屏模式hotseat在右侧显示,平板是false
boolean isPortrait = !mLauncher.getDeviceProfile().isVerticalBarLayout();
int hotseatItemsNum = mLauncher.getDeviceProfile().numShownHotseatIcons;
for (int i = 0; i < hotseatItemsNum; i++) {
int x = isPortrait ? i : 0;
int y = isPortrait ? 0 : hotseatItemsNum - i - 1;
//循环获取当前hotseat里的child
View child = mHotseat.getChildAt(x, y);
if (child == null || child.getTag() == null) continue;
ItemInfo tag = (ItemInfo) child.getTag();
//如果child本身就是推荐类型的,不做处理
if (tag.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) continue;
//修改数据里的存储位置
mLauncher.getModelWriter().moveItemInDatabase(tag,
LauncherSettings.Favorites.CONTAINER_DESKTOP, pageId, i, toRow);
//加入集合
mNewItems.add(tag);
}
return pageId;
}
>2.getSettingsIntent
public static final String SETTINGS_ACTION =
"android.settings.ACTION_CONTENT_SUGGESTIONS_SETTINGS";
static Intent getSettingsIntent() {
return new Intent(SETTINGS_ACTION).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
13.2.moveHotseatItems
void moveHotseatItems() {
//清空hotaset容器
mHotseat.removeAllViewsInLayout();
//数据来源13.1
if (!mNewItems.isEmpty()) {
int lastPage = mNewItems.get(mNewItems.size() - 1).screenId;
ArrayList<ItemInfo> animated = new ArrayList<>();
ArrayList<ItemInfo> nonAnimated = new ArrayList<>();
for (ItemInfo info : mNewItems) {
if (info.screenId == lastPage) {
animated.add(info);
} else {
nonAnimated.add(info);
}
}
//重新绑定数据
mLauncher.bindAppsAdded(mNewScreens, nonAnimated, animated);
}
}
13.3.showDimissTip
void showDimissTip() {
//hotseat当前显示的child个数不满
if (mHotseat.getShortcutsAndWidgets().getChildCount()
< mLauncher.getDeviceProfile().numShownHotseatIcons) {
//提示文字: 应用建议已添加到空白区域
Snackbar.show(mLauncher, R.string.hotseat_tip_gaps_filled,
R.string.hotseat_prediction_settings, null,
//设置按钮同13.1.2
() -> mLauncher.startActivity(getSettingsIntent()));
} else {
showHotseatArrowTip(true, mLauncher.getString(R.string.hotseat_tip_no_empty_slots));//将应用拖离底部,以获取应用建议
}
}
>1.showHotseatArrowTip
- usePinned为true表示查找第一个pinned数据,为false则使用第一个找到的predicted数据
- 作用就是找到合适的child,上边显示个带箭头的文字提示
private boolean showHotseatArrowTip(boolean usePinned, String message) {
int childCount = mHotseat.getShortcutsAndWidgets().getChildCount();
boolean isPortrait = !mLauncher.getDeviceProfile().isVerticalBarLayout();
BubbleTextView tipTargetView = null;
for (int i = childCount - 1; i > -1; i--) {
int x = isPortrait ? i : 0;
int y = isPortrait ? 0 : i;
View v = mHotseat.getShortcutsAndWidgets().getChildAt(x, y);
if (v instanceof BubbleTextView && v.getTag() instanceof WorkspaceItemInfo) {
ItemInfo info = (ItemInfo) v.getTag();
boolean isPinned = info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT;
if (isPinned == usePinned) {
tipTargetView = (BubbleTextView) v;
break;
}
}
}
if (tipTargetView == null) {
Log.e(TAG, "Unable to find suitable view for ArrowTip");
return false;
}
Rect bounds = Utilities.getViewBounds(tipTargetView);
new ArrowTipView(mLauncher).show(message, Gravity.END, bounds.centerX(), bounds.top);
return true;
}
13.4.showEdu
- requiresMigration为true说明当前hotseat里有pinned的数据
- canMigrateToFirstPage
void showEdu() {
//hotseat下的child个数
int childCount = mHotseat.getShortcutsAndWidgets().getChildCount();
CellLayout cellLayout = mLauncher.getWorkspace().getScreenWithId(Workspace.FIRST_SCREEN_ID);
// hotseat is already empty and does not require migration. show edu tip
//循环所有的child,如果非predicted的数据,返回true
boolean requiresMigration = IntStream.range(0, childCount).anyMatch(i -> {
View v = mHotseat.getShortcutsAndWidgets().getChildAt(i);
return v != null && v.getTag() != null && ((ItemInfo) v.getTag()).container
!= LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
});
//有点复杂,就是判断是否有足够的控件展示数据?
boolean canMigrateToFirstPage = cellLayout.makeSpaceForHotseatMigration(false);
if (requiresMigration && canMigrateToFirstPage) {
showDialog();//补充1,推荐提示框
} else {
//不满足显示弹框的条件,那么这里在合适的图标上边显示个带箭头的提示文字
if (showHotseatArrowTip(requiresMigration, mLauncher.getString(
//requiresMigration为true,文字为:将应用拖离底部,以获取应用建议
//requiresMigration为false,文字为:最常用的应用会显示在此处,显示的项目会根据日常安排而发生变化
requiresMigration ? R.string.hotseat_tip_no_empty_slots
: R.string.hotseat_auto_enrolled))) {
}
finishOnboarding();
}
}
>1.showDialog
void showDialog() {
if (mPredictedApps == null || mPredictedApps.isEmpty()) {
//没有推荐数据
return;
}
if (mActiveDialog != null) {
//关闭旧的
mActiveDialog.handleClose(false);
}
//创建新的
mActiveDialog = HotseatEduDialog.getDialog(mLauncher);
mActiveDialog.setHotseatEduController(this);
//显示
mActiveDialog.show(mPredictedApps);
}
14.HotseatPredictionController.java
14.1.showEdu
显示推荐应用提示框
public void showEdu() {
//首先回到normal状态,也就是默认的桌面
mLauncher.getStateManager().goToState(NORMAL, true, forSuccessCallback(() -> {
//新建一个对象
HotseatEduController eduController = new HotseatEduController(mLauncher);
//设置推荐数据
eduController.setPredictedApps(mPredictedItems.stream()
.map(i -> (WorkspaceItemInfo) i)
.collect(Collectors.toList()));
//显示,13.4
eduController.showEdu();
}));
}
14.2.mPredictedItems数据
来源这里简单贴下,其他帖子里学习过了。
- QuickstepModelDelegate类里有注册监听predicted数据的变化
- 数据变化后设置给QuickstepLauncher.java里的bindExtraContainerItems,这里边会调用HotseatPredictionController.java对象里的setPredictedItems方法更新数据
15.QuickstepOnboardingPrefs
新用户引导控制台
public class QuickstepOnboardingPrefs extends OnboardingPrefs<QuickstepLauncher> {
15.1.构造方法
- HOME_BOUNCE_SEEN的作用??
- HOTSEAT_DISCOVERY_TIP_COUNT的作用见小节14,效果见小节10,弹个提示框
- ALL_APPS_VISITED_COUNT的作用见小节7 AppsDividerView,决定是显示一条线还是一个文字提示
- TYPE_ALL_APPS_EDU提示效果见17,与HINT_STATE状态有关,暂时不清楚这是啥状态??
public QuickstepOnboardingPrefs(QuickstepLauncher launcher, SharedPreferences sharedPrefs) {
super(launcher, sharedPrefs);
StateManager<LauncherState> stateManager = launcher.getStateManager();
//待研究?
if (!getBoolean(HOME_BOUNCE_SEEN)) {
stateManager.addStateListener(new StateListener<LauncherState>() {
@Override
public void onStateTransitionComplete(LauncherState finalState) {
//手势导航模式
boolean swipeUpEnabled =
DisplayController.getNavigationMode(mLauncher).hasGestures;
//旧状态
LauncherState prevState = stateManager.getLastState();
//条件1:手势导航,最终状态是recent
//条件2:非手势导航,最终状态是allApps
//条件3:对应的事件触发次数达到最大值
if (((swipeUpEnabled && finalState == OVERVIEW) || (!swipeUpEnabled
&& finalState == ALL_APPS && prevState == NORMAL) ||
hasReachedMaxCount(HOME_BOUNCE_COUNT))) {
//修改为true
mSharedPrefs.edit().putBoolean(HOME_BOUNCE_SEEN, true).apply();
//移除监听
stateManager.removeStateListener(this);
}
}
});
}
//HOTSEAT_DISCOVERY_TIP_COUNT对应的事件没有达到最大值,添加状态监听
if (!Utilities.IS_RUNNING_IN_TEST_HARNESS
&& !hasReachedMaxCount(HOTSEAT_DISCOVERY_TIP_COUNT)) {
stateManager.addStateListener(new StateListener<LauncherState>() {
boolean mFromAllApps = false;
@Override
public void onStateTransitionStart(LauncherState toState) {
//起始状态是allApps
mFromAllApps = mLauncher.getStateManager().getCurrentStableState() == ALL_APPS;
}
@Override
public void onStateTransitionComplete(LauncherState finalState) {
HotseatPredictionController client = mLauncher.getHotseatPredictionController();
//从allApps页面回到默认桌面,并且有推荐数据
if (mFromAllApps && finalState == NORMAL && client.hasPredictions()) {
//增加事件次数,见16.3
if (incrementEventCount(HOTSEAT_DISCOVERY_TIP_COUNT)) {
//到达最大值,显示提示
client.showEdu();
//移除监听
stateManager.removeStateListener(this);
}
}
}
});
}
//手势导航前提下
if (DisplayController.getNavigationMode(launcher) == NO_BUTTON) {
stateManager.addStateListener(new StateListener<LauncherState>() {
//滑动触发edu的最大次数
private static final int MAX_NUM_SWIPES_TO_TRIGGER_EDU = 3;
// Counts the number of consecutive swipes on nav bar without moving screens.
private int mCount = 0;
private boolean mShouldIncreaseCount;
@Override
public void onStateTransitionStart(LauncherState toState) {
if (toState == NORMAL) {
//目标状态是normal,不做处理
return;
}
//是否应该增加触发次数,2个条件
mShouldIncreaseCount = toState == HINT_STATE
&& launcher.getWorkspace().getNextPage() == Workspace.DEFAULT_PAGE;
}
@Override
public void onStateTransitionComplete(LauncherState finalState) {
if (finalState == NORMAL) {
//到达最大值
if (mCount >= MAX_NUM_SWIPES_TO_TRIGGER_EDU) {
//allapp的相关提示不存在
if (getOpenView(mLauncher, TYPE_ALL_APPS_EDU) == null) {
//显示对应的ui
AllAppsEduView.show(launcher);
}
//重置count
mCount = 0;
}
return;
}
//应该增加count并且最终状态是hint的话,count加一,否则重置为0
if (mShouldIncreaseCount && finalState == HINT_STATE) {
mCount++;
} else {
mCount = 0;
}
//最终状态是allApps
if (finalState == ALL_APPS) {
//如果有在现实的allAppsEdu提示ui,那么隐藏
AllAppsEduView view = getOpenView(mLauncher, TYPE_ALL_APPS_EDU);
if (view != null) {
view.close(false);
}
}
}
});
}
//切换到allApps页面的次数没有到达最大值
if (!hasReachedMaxCount(ALL_APPS_VISITED_COUNT)) {
mLauncher.getStateManager().addStateListener(new StateListener<LauncherState>() {
@Override
public void onStateTransitionComplete(LauncherState finalState) {
if (finalState == ALL_APPS) {
//allapps页面,到达一次,次数加一
incrementEventCount(ALL_APPS_VISITED_COUNT);
return;//返回
}
//非allApps页面,判断是否到达最大值
boolean hasReachedMaxCount = hasReachedMaxCount(ALL_APPS_VISITED_COUNT);
//默认显示all apps标签,到达最大值以后不再显示
mLauncher.getAppsView().getFloatingHeaderView().findFixedRowByType(
AppsDividerView.class).setShowAllAppsLabel(!hasReachedMaxCount);
if (hasReachedMaxCount) {
//到达最大值,移除监听
mLauncher.getStateManager().removeStateListener(this);
}
}
});
}
}
16.父类OnboardingPrefs.java
16.1.MAX_COUNTS
不同事件定义的最大次数,到达最大值会做一些事情
private static final Map<String, Integer> MAX_COUNTS;
static {
Map<String, Integer> maxCounts = new ArrayMap<>(5);
maxCounts.put(HOME_BOUNCE_COUNT, 3);
maxCounts.put(HOTSEAT_DISCOVERY_TIP_COUNT, 5);
maxCounts.put(SEARCH_SNACKBAR_COUNT, 3);
// This is the sum of all onboarding cards. Currently there is only 1 card shown 3 times.
maxCounts.put(SEARCH_ONBOARDING_COUNT, 3);
maxCounts.put(ALL_APPS_VISITED_COUNT, 20);
MAX_COUNTS = Collections.unmodifiableMap(maxCounts);
}
16.2.hasReachedMaxCount
判断给定的事件是否达到设置的最大数
public boolean hasReachedMaxCount(@EventCountKey String eventKey) {
return hasReachedMaxCount(getCount(eventKey), eventKey);
}
private boolean hasReachedMaxCount(int count, @EventCountKey String eventKey) {
//当前值和最大值比较
return count >= MAX_COUNTS.get(eventKey);
}
>1.getCount
读取本地存储的值
public int getCount(@EventCountKey String key) {
return mSharedPrefs.getInt(key, 0);
}
16.3.incrementEventCount
增加前后判断是否到达最大值
public boolean incrementEventCount(@EventCountKey String eventKey) {
int count = getCount(eventKey);
//见16.2
if (hasReachedMaxCount(count, eventKey)) {
//到达最大值
return true;
}
count++;//次数加一并保存
mSharedPrefs.edit().putInt(eventKey, count).apply();
//再次判断
return hasReachedMaxCount(count, eventKey);
}
16.4.ALL_PREF_KEYS
如果有新加的key,记得也放到这个map里,方便到时候reset
public static final Map<String, String[]> ALL_PREF_KEYS = Map.of(
"All Apps Bounce", new String[] { HOME_BOUNCE_SEEN, HOME_BOUNCE_COUNT },
"Hybrid Hotseat Education", new String[] { HOTSEAT_DISCOVERY_TIP_COUNT,
HOTSEAT_LONGPRESS_TIP_SEEN },
"Search Education", new String[] { SEARCH_KEYBOARD_EDU_SEEN, SEARCH_SNACKBAR_COUNT,
SEARCH_ONBOARDING_COUNT, QSB_SEARCH_ONBOARDING_CARD_DISMISSED},
"Taskbar Education", new String[] { TASKBAR_EDU_SEEN },
"All Apps Visited Count", new String[] {ALL_APPS_VISITED_COUNT}
);
16.4.launcher settings
上边的ALL_PREF_KEYS是在launcher的settings页面里用到了。
>1.launcher_preferences.xml
选项如下:
<androidx.preference.PreferenceScreen
android:key="pref_developer_options"
android:persistent="false"
android:title="@string/developer_options_title"
android:fragment="com.android.launcher3.settings.DeveloperOptionsFragment"/>
ps:这个是aosp里默认的布局,如果带有谷歌的GMS套件的话,可能被重写,不一定用的这个,仅供参考。
>2.updateDeveloperOption
case DEVELOPER_OPTIONS_KEY:
mDeveloperOptionPref = preference;
return updateDeveloperOption();
}
private boolean updateDeveloperOption() {
//选项是否可见
boolean showPreference = FeatureFlags.showFlagTogglerUi(getContext())
|| PluginManagerWrapper.hasPlugins(getContext());//这个值是测试用的
if (mDeveloperOptionPref != null) {
mDeveloperOptionPref.setEnabled(showPreference);
if (showPreference) {
getPreferenceScreen().addPreference(mDeveloperOptionPref);
} else {
getPreferenceScreen().removePreference(mDeveloperOptionPref);
}
}
return showPreference;
}
可以看到,显示的条件是: debug系统并且开发者模式打开
public static boolean showFlagTogglerUi(Context context) {
return Utilities.IS_DEBUG_DEVICE && Utilities.isDevelopersOptionsEnabled(context);
}
public static final boolean IS_DEBUG_DEVICE =
Build.TYPE.toLowerCase(Locale.ROOT).contains("debug") ||
Build.TYPE.toLowerCase(Locale.ROOT).equals("eng");
public static boolean isDevelopersOptionsEnabled(Context context) {
return Settings.Global.getInt(context.getApplicationContext().getContentResolver(),
Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0;
}
>3.DeveloperOptionsFragment.java
这个就是添加的reset选项
private void addOnboardingPrefsCatergory() {
PreferenceCategory onboardingCategory = newCategory("Onboarding Flows");
onboardingCategory.setSummary("Reset these if you want to see the education again.");
//循环map,每个entry添加一个选项
for (Map.Entry<String, String[]> titleAndKeys : OnboardingPrefs.ALL_PREF_KEYS.entrySet()) {
String title = titleAndKeys.getKey();
//每个entry包含的key集合
String[] keys = titleAndKeys.getValue();
Preference onboardingPref = new Preference(getContext());
onboardingPref.setTitle(title);
onboardingPref.setSummary("Tap to reset");
//点击事件
onboardingPref.setOnPreferenceClickListener(preference -> {
SharedPreferences.Editor sharedPrefsEdit = LauncherPrefs.getPrefs(getContext())
.edit();
//点击以后就是循环选项所包含的key,删除对应的数据
for (String key : keys) {
sharedPrefsEdit.remove(key);
}
sharedPrefsEdit.apply();
return true;
});
onboardingCategory.addPreference(onboardingPref);
}
}
17.AllAppsEduView.java
这个视图是用来展示在手势模式下如何进入allApps页面
public class AllAppsEduView extends AbstractFloatingView {
17.1.构造方法
public AllAppsEduView(Context context, AttributeSet attrs) {
super(context, attrs);
//64dp的实心圆
mCircle = (GradientDrawable) context.getDrawable(R.drawable.all_apps_edu_circle);
mCircleSizePx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_circle_size);
mPaddingPx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_padding);
mWidthPx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_width);
mMaxHeightPx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_max_height);
setWillNotDraw(false);
}
17.2.show
public static void show(Launcher launcher) {
final DragLayer dragLayer = launcher.getDragLayer();
//加载布局,见补充1
AllAppsEduView view = (AllAppsEduView) launcher.getLayoutInflater().inflate(
R.layout.all_apps_edu_view, dragLayer, false);
//见17.3
view.init(launcher);
//加入容器里
launcher.getDragLayer().addView(view);
//刷新布局并播放动画
view.requestLayout();
view.playAnimation();
}
>1.all_apps_edu_view.xml
<com.android.quickstep.views.AllAppsEduView
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="@dimen/swipe_edu_width"
android:layout_height="@dimen/swipe_edu_max_height"/>
17.3.init
private void init(Launcher launcher) {
mLauncher = launcher;
mTouchController = new AllAppsEduTouchController(mLauncher);
int accentColor = Themes.getColorAccent(launcher);
//渐变色图
mGradient = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM,
Themes.getAttrBoolean(launcher, R.attr.isMainColorDark)
? new int[]{0xB3FFFFFF, 0x00FFFFFF}
: new int[]{ColorUtils.setAlphaComponent(accentColor, 127),
ColorUtils.setAlphaComponent(accentColor, 0)});
float r = mWidthPx / 2f;
mGradient.setCornerRadii(new float[]{r, r, r, r, 0, 0, 0, 0});
int top = mMaxHeightPx - mCircleSizePx + mPaddingPx;
//设置drawable尺寸
mCircle.setBounds(mPaddingPx, top, mPaddingPx + mCircleSizePx, top + mCircleSizePx);
mGradient.setBounds(0, mMaxHeightPx - mCircleSizePx, mWidthPx, mMaxHeightPx);
DeviceProfile grid = launcher.getDeviceProfile();
DragLayer.LayoutParams lp = new DragLayer.LayoutParams(mWidthPx, mMaxHeightPx);
lp.ignoreInsets = true;
lp.leftMargin = (grid.widthPx - mWidthPx) / 2;
lp.topMargin = grid.heightPx - grid.hotseatBarSizePx - mMaxHeightPx;
//更新布局参数
setLayoutParams(lp);
}
18.TaskbarEduController
18.1.showEdu
void showEdu() {
TaskbarOverlayController overlayController = mControllers.taskbarOverlayController;
TaskbarOverlayContext overlayContext = overlayController.requestWindow();
LayoutInflater layoutInflater = overlayContext.getLayoutInflater();
//加载布局,见补充1
mTaskbarEduView = (TaskbarEduView) layoutInflater.inflate(
R.layout.taskbar_edu, overlayContext.getDragLayer(), false);
//获取里边的翻页容器
mPagedView = mTaskbarEduView.findViewById(R.id.content);
//给翻页容器里根据手势模式或者三键导航模式添加不同的page
layoutInflater.inflate(
DisplayController.isTransientTaskbar(overlayContext)
? R.layout.taskbar_edu_pages_transient
: R.layout.taskbar_edu_pages_persistent,
mPagedView,
true);
// Provide enough room for taskbar.
View startButton = mTaskbarEduView.findViewById(R.id.edu_start_button);
ViewGroup.MarginLayoutParams layoutParams =
(ViewGroup.MarginLayoutParams) startButton.getLayoutParams();
DeviceProfile dp = overlayContext.getDeviceProfile();
layoutParams.bottomMargin += DisplayController.isTransientTaskbar(overlayContext)
? dp.taskbarSize + dp.transientTaskbarMargin
: dp.taskbarSize;
//见19.1,以及18.2
mTaskbarEduView.init(new TaskbarEduCallbacks());
mControllers.navbarButtonsViewController.setSlideInViewVisible(true);
mTaskbarEduView.setOnCloseBeginListener(
() -> mControllers.navbarButtonsViewController.setSlideInViewVisible(false));
mTaskbarEduView.addOnCloseListener(() -> mTaskbarEduView = null);
//见19.2加入容器
mTaskbarEduView.show();
}
>1.taskbar_edu.xml
- TaskbarEduView里套个ConstraintLayout
- ConstraintLayout里上边显示TaskbarEduPagedView(里边包含多个page)
- 底部依次是start按钮,indicator,以及end按钮
<com.android.launcher3.taskbar.TaskbarEduView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:gravity="bottom"
android:orientation="vertical"
android:layout_marginHorizontal="108dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/edu_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_rounded_corner_bottom_sheet"
android:gravity="center_horizontal"
android:paddingHorizontal="36dp"
android:paddingTop="64dp">
<com.android.launcher3.taskbar.TaskbarEduPagedView
android:id="@+id/content"
android:clipToPadding="false"
android:layout_width="match_parent"
android:layout_height="378dp"
app:layout_constraintTop_toTopOf="parent"
launcher:pageIndicator="@+id/content_page_indicator"/>
<Button
android:id="@+id/edu_start_button"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_marginBottom="92dp"
android:layout_marginTop="32dp"
app:layout_constraintTop_toBottomOf="@id/content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:text="@string/taskbar_edu_close"
style="@style/TaskbarEdu.Button.Close"
android:textColor="?android:attr/textColorPrimary"/>
<com.android.launcher3.pageindicators.PageIndicatorDots
android:id="@+id/content_page_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="@id/edu_start_button"
app:layout_constraintBottom_toBottomOf="@id/edu_start_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:elevation="1dp" />
<Button
android:id="@+id/edu_end_button"
android:layout_width="wrap_content"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="@id/edu_start_button"
app:layout_constraintBottom_toBottomOf="@id/edu_start_button"
app:layout_constraintEnd_toEndOf="parent"
android:text="@string/taskbar_edu_next"
style="@style/TaskbarEdu.Button.Next"
android:textColor="?androidprv:attr/textColorOnAccent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.android.launcher3.taskbar.TaskbarEduView>
18.2.TaskbarEduCallbacks
class TaskbarEduCallbacks {
void onPageChanged(int prevPage, int currentPage, int pageCount) {
// Reset previous pages' animation.
LottieAnimationView prevAnimation = mPagedView.getChildAt(prevPage)
.findViewById(R.id.animation);
prevAnimation.cancelAnimation();
prevAnimation.setFrame(0);
mPagedView.getChildAt(currentPage)
.<LottieAnimationView>findViewById(R.id.animation)
.playAnimation();
//根据当前页,决定第一个按钮显示内容以及点击事假
if (currentPage == 0) {
mTaskbarEduView.updateStartButton(R.string.taskbar_edu_close,
v -> mTaskbarEduView.close(true /* animate */));
} else {
mTaskbarEduView.updateStartButton(R.string.taskbar_edu_previous,
v -> mTaskbarEduView.snapToPage(currentPage - 1));
}
//同样的,最后一页,
if (currentPage == pageCount - 1) {
mTaskbarEduView.updateEndButton(R.string.taskbar_edu_done,
v -> mTaskbarEduView.close(true /* animate */));
} else {
mTaskbarEduView.updateEndButton(R.string.taskbar_edu_next,
v -> mTaskbarEduView.snapToPage(currentPage + 1));
}
}
int getIconLayoutBoundsWidth() {
return mControllers.taskbarViewController.getIconLayoutWidth();
}
int getOpenDuration() {
return mControllers.taskbarOverlayController.getOpenDuration();
}
int getCloseDuration() {
return mControllers.taskbarOverlayController.getCloseDuration();
}
}
18.3.showEdu调用逻辑
>1.LauncherTaskbarUIController.java
从逻辑来看,这个东西应该只会显示一次。
public void showEdu() {
if (!shouldShowEdu()) {
return;
}
// TASKBAR_EDU_SEEN的值标记为true
mLauncher.getOnboardingPrefs().markChecked(OnboardingPrefs.TASKBAR_EDU_SEEN);
//走到小节18.1
mControllers.taskbarEduController.showEdu();
}
- TASKBAR_EDU_SEEN的值为false
public boolean shouldShowEdu() {
return !Utilities.IS_RUNNING_IN_TEST_HARNESS
&& !mLauncher.getOnboardingPrefs().getBoolean(OnboardingPrefs.TASKBAR_EDU_SEEN);
}
>2.QuickstepTransitionManager.java
- 获取控制从应用程序图标打开目标窗口的Animator
- 点击应用图标,应用打开的动画结束以后,会显示edu,而从补充1的逻辑,只会显示一次。
private Animator getOpeningWindowAnimators(View v,
//...
appAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
LauncherTaskbarUIController taskbarController = mLauncher.getTaskbarUIController();
if (taskbarController != null && taskbarController.shouldShowEdu()) {
// LAUNCHER_TASKBAR_EDUCATION_SHOWING is set to true here, when the education
// flow is about to start, to avoid a race condition with other components
// that would show something else to the user as soon as the app is opened.
Settings.Secure.putInt(mLauncher.getContentResolver(),
LAUNCHER_TASKBAR_EDUCATION_SHOWING, 1);
}
}
@Override
public void onAnimationEnd(Animator animation) {
if (v instanceof BubbleTextView) {
((BubbleTextView) v).setStayPressed(false);
}
LauncherTaskbarUIController taskbarController = mLauncher.getTaskbarUIController();
if (taskbarController != null) {
//这里显示提示
taskbarController.showEdu();
}
openingTargets.release();
}
});
19.TaskbarEduView
public class TaskbarEduView extends AbstractSlideInView<TaskbarOverlayContext>
implements Insettable {
19.1.init
主要就是给里边的pageView添加回调
protected void init(TaskbarEduController.TaskbarEduCallbacks callbacks) {
if (mPagedView != null) {
mPagedView.setControllerCallbacks(callbacks);
}
mTaskbarEduCallbacks = callbacks;
}
19.2.show
public void show() {
attachToContainer();//补充1
animateOpen();//开启动画
}
>1.attachToContainer
就是把自己加入容器里
protected void attachToContainer() {
if (mColorScrim != null) {
//如果有蒙版的话先加入
getPopupContainer().addView(mColorScrim, 0);
}
getPopupContainer().addView(this, 1);
}
19.3.isOfType
类型如下
protected boolean isOfType(int type) {
return (type & TYPE_TASKBAR_EDUCATION_DIALOG) != 0;
}