1.Launcher簡介
Launcher是安卓系統(tǒng)中的桌面啟動(dòng)器,安卓系統(tǒng)的桌面UI統(tǒng)稱為Launcher。Launcher是安卓系統(tǒng)中的主要程序組件之一,安卓系統(tǒng)中如果沒有Launcher就無法啟動(dòng)安卓桌面。作為車機(jī)開機(jī)后用戶接觸到的第一個(gè)帶有界面的系統(tǒng)級APP,和普通APP一樣,它的界面也是在Activity上繪制出來的。
車機(jī)上Launcher一般分為兩個(gè)界面,首頁和應(yīng)用列表界面。
首頁一般包括用戶信息、常用應(yīng)用快捷方式、3D車模和widget卡片,widget卡片有:地圖、天氣、音樂播放器、時(shí)鐘等;
應(yīng)用列表界面就是啟動(dòng)APP的列表界面,單擊APP的Icon可進(jìn)入App,長按APP的Icon可以進(jìn)入編輯模式,編輯模式下APP可以進(jìn)行拖拽、合并文件夾、刪除等功能。
(ps:頂部狀態(tài)欄status bar和底部導(dǎo)航欄navigation bar屬于System UI,中間才屬于Launcher部分)
2.Widget概述
參考資料:應(yīng)用微件概覽
Widget,又稱為微件或者小部件。我們可以把它當(dāng)作是一個(gè)微型應(yīng)用程序視圖,用以嵌入到其他應(yīng)用程序中(一般來說就是桌面Launcher)并接收周期性的更新。這樣用戶就可以方便查看應(yīng)用程序的重點(diǎn)信息或者進(jìn)行應(yīng)用程序的快捷控制。
Widget類型官方分為信息微件、集合微件、控制微件和混合微件。開發(fā)Widget是由各自應(yīng)用程序(如天氣、導(dǎo)航、音樂)開發(fā)人員開發(fā),不是本篇的重點(diǎn)內(nèi)容,網(wǎng)上有很多關(guān)于Widget開發(fā)的例子。如何使車載Launcher具有擺放Widget的能力,是我們關(guān)注的重點(diǎn)!
3.Launcher開發(fā)如何顯示W(wǎng)idget
3.1 使Launcher App成為系統(tǒng)級App
-
Q:為什么在顯示W(wǎng)idget的時(shí)候要把Launcher App聲明為系統(tǒng)級的App呢?
-
A:開發(fā)Launcher App時(shí)肯定會聲明其為系統(tǒng)級App。而顯示W(wǎng)idget時(shí)需要App是系統(tǒng)級的原因是:Widget顯示需要我們獲取到AppWidgetManager對象并調(diào)用**public boolean bindAppWidgetIdIfAllowed(int appWidgetId, ComponentName provider)**方法,而此方法返回值要為true就需要App是系統(tǒng)級App。
private AppWidgetProviderInfo createAppWidgetInfo(ComponentName component) {
//分配新的widgetId
int widgetId = LauncherApplication.getContext().getWidgetHost().allocateAppWidgetId();
//將widgetId和ComponentName綁定
boolean isBindAppWidgetIdIfAllowed = LauncherApplication.getContext()
.getWidgetManager().bindAppWidgetIdIfAllowed(widgetId, component);
LogUtil.info(TAG, "createAppWidgetInfo bindAppWidgetIdIfAllowed = "
+ isBindAppWidgetIdIfAllowed);
//獲取AppWidgetProviderInfo
AppWidgetProviderInfo appWidgetInfo = LauncherApplication.getContext()
.getWidgetManager().getAppWidgetInfo(widgetId);
//存儲widgetId、包名、類名到數(shù)據(jù)庫
WidgetInfoEntity entity = new WidgetInfoEntity(widgetId, component.getPackageName(),
component.getClassName(), checkWidgetDisplay(component.getPackageName()));
saveWidgetInfo(entity);
return appWidgetInfo;
}
Launcher未聲明為系統(tǒng)級App時(shí)截取的Log:
將App聲明為系統(tǒng)級App的步驟:
- 將車機(jī)系統(tǒng)簽名放到項(xiàng)目中,創(chuàng)建一個(gè)keystore目錄放置簽名文件:
- app目錄下的build.gradle文件配置簽名文件,在android{}內(nèi)加上簽名文件的配置信息,然后sync一下:
android {
...
signingConfigs {
config {
storeFile file('../keystore/platform.jks')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
signingConfig signingConfigs.config
}
debug {
signingConfig signingConfigs.config
}
}
...
}
3.在AndroidManifest.xml文件添加android:sharedUserId=”android.uid.system”,讓程序運(yùn)行在系統(tǒng)進(jìn)程中。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.yx.yxlauncher"
android:sharedUserId="android.uid.system">
//定義查詢權(quán)限,查詢系統(tǒng)中的所有widget廣播需要
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
...
</manifest>
通過以上步驟,相當(dāng)于把我們自己的開發(fā)的Launcher聲明成系統(tǒng)級App了。
3.2 定義并初始化AppWidgetHost對象
定義類繼承Application,在Application初始化的時(shí)候定義好AppWidgetHost對象并且調(diào)用**startListening()**方法
public class YxApplication extends Application {
private static final String TAG = "Yx_YxApplication";
private AppWidgetHost mWidgetHost;
//自定義一個(gè)APPWIDGET_HOST_ID
private static final int APPWIDGET_HOST_ID = 0x300;
private static YxApplication sApplication;
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate: ");
sApplication = this;
initWidgetHost();
}
private void initWidgetHost() {
//初始化WidgetHost并且開始接收onAppWidgetChanged()的回調(diào)
mWidgetHost = new AppWidgetHost(YxApplication.getContext(), APPWIDGET_HOST_ID);
mWidgetHost.startListening();
//初始化數(shù)據(jù)庫里存儲的widget信息列表,后面會介紹數(shù)據(jù)庫存儲的內(nèi)容
WidgetInfoManager.getInstance().initializeWidget();
//初始化Widget廣播的ResolveInfo列表
WidgetInfoManager.getInstance().initializeWidgetResolveInfo();
}
public static YxApplication getContext() {
return sApplication;
}
public static Context getDirectBootContext() {
return getContext().getBaseContext().createDeviceProtectedStorageContext();
}
public AppWidgetManager getWidgetManager() {
return AppWidgetManager.getInstance(YxApplication.getContext());
}
public AppWidgetHost getWidgetHost() {
return mWidgetHost;
}
3.3 數(shù)據(jù)庫存儲widgetId
有了我們的AppWidgetHost,我們就可以調(diào)用**allocateAppWidgetId()**方法獲取widgetId,并且將其存入數(shù)據(jù)庫,定義實(shí)體類WidgetInfoEntity,我用的是room數(shù)據(jù)庫,存儲了widgetId、包名、類名:
@Entity(tableName = "widget_info")
public class WidgetInfoEntity {
@PrimaryKey
@ColumnInfo(name = "widgetId")
private int widgetId;
@ColumnInfo(name = "packageName")
private String packageName;
@ColumnInfo(name = "className")
private String className;
/**
* Construction method.
*/
public WidgetInfoEntity(int widgetId, String packageName, String className) {
this.widgetId = widgetId;
this.packageName = packageName;
this.className = className;
}
public int getWidgetId() {
return widgetId;
}
public String getPackageName() {
return packageName;
}
public String getClassName() {
return className;
}
@Override
public String toString() {
return "WidgetInfoEntity{" +
"widgetId=" + widgetId +
", packageName='" + packageName + '\'' +
", className='" + className + '\'' +
'}';
}
}
dao層定義,將訪問數(shù)據(jù)庫里的widget信息的代碼封裝起來:
@Dao
public interface WidgetInfoDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertWidgetInfo(WidgetInfoEntity... infoEntity);
@Query("SELECT * FROM " + "widget_info" + " ORDER BY " + "widgetId" + " ASC")
List<WidgetInfoEntity> queryAllWidgetInfos();
@Delete
void deleteWidgetInfo(WidgetInfoEntity entity);
}
db定義,數(shù)據(jù)庫工具類,包含創(chuàng)建數(shù)據(jù)庫、打開數(shù)據(jù)庫、數(shù)據(jù)庫操作的對外方法等:
@Database(entities = {WidgetInfoEntity.class}, version = 1, exportSchema = false)
public abstract class DatabaseUtil extends RoomDatabase {
private static final String TAG = "Yx_DatabaseUtil";
private static DatabaseUtil sInstance;
private final ExecutorService mExecutor;
private final WidgetInfoDao mWidgetInfoDao;
public DatabaseUtil() {
mExecutor = Executors.newSingleThreadExecutor();
mWidgetInfoDao = widgetInfoDao();
}
/**
* get DatabaseUtil Singleton.
*
* @return DatabaseUtil
*/
public static DatabaseUtil getInstance() {
if (sInstance == null) {
synchronized (DatabaseUtil.class) {
create();
}
}
return sInstance;
}
private static void create() {
Log.i(TAG, "create: ");
sInstance = Room.databaseBuilder(YxApplication.getDirectBootContext(),
DatabaseUtil.class, "yx_launcher_db")
.addCallback(new RoomDatabase.Callback() {
@Override
public void onCreate(@NonNull SupportSQLiteDatabase db) {
super.onCreate(db);
Log.d(TAG, "onCreate database: " + db.getPath());
}
@Override
public void onOpen(@NonNull SupportSQLiteDatabase db) {
super.onOpen(db);
Log.d(TAG, "onOpen database: " + db.getPath());
}
}).allowMainThreadQueries()
.fallbackToDestructiveMigration()
.build();
}
/**
* Create instance of WidgetInfoDao.
*
* @return WidgetInfoDao.
*/
public abstract WidgetInfoDao widgetInfoDao();
/**
* Query all widgetInfo.
*
* @return widgetInfos
*/
public List<WidgetInfoEntity> queryAllWidgetInfos() {
Log.d(TAG, "queryAllWidgetInfos: ");
return mWidgetInfoDao.queryAllWidgetInfos();
}
/**
* insert WidgetInfoEntity.
*
* @param infoEntity WidgetInfoEntity
*/
public void insertWidgetInfos(WidgetInfoEntity infoEntity) {
Log.d(TAG, "insertWidgetInfos: infoEntity = " + infoEntity.toString());
mExecutor.execute(() -> mWidgetInfoDao.insertWidgetInfo(infoEntity));
}
/**
* Delete WidgetInfo.
*
* @param entity WidgetInfoEntity
*/
public void deleteWidgetInfo(WidgetInfoEntity entity) {
Log.d(TAG, "deleteWidgetInfo: entity = " + entity);
mExecutor.execute(() -> mWidgetInfoDao.deleteWidgetInfo(entity));
}
}
3.4 定義WidgetInfoManager類處理widget
包含的內(nèi)容:
- 查詢系統(tǒng)里所有Widget廣播的ResolveInfo列表用于獲取其ComponentName
- 根據(jù)存儲的widgetId獲取或者新創(chuàng)建AppWidgetProviderInfo
- 保存新創(chuàng)建的widgetId到數(shù)據(jù)庫,刪除數(shù)據(jù)庫里數(shù)據(jù)或者去重
其實(shí),這個(gè)類最主要的目的就是拿到AppWidgetProviderInfo對象,有了這個(gè)對象才能獲取AppWidgetHostView用于顯示:
public class WidgetInfoManager {
private static final String TAG = "Yx_WidgetInfoManager";
private static final long RELOAD_DELAY = 100;
private final List<WidgetInfoEntity> mWidgetInfoList = new ArrayList<>();
private final List<ResolveInfo> mAllWidgetResolveInfo = new ArrayList<>();
private final Handler mHandler = new Handler(YxApplication.getContext().getMainLooper());
private final Runnable mReloadWidgetResolveInfoRunnable
= this::initializeWidgetResolveInfo;
private static class SingletonHolder {
// Static initializer, thread safety is guaranteed by JVM
private static WidgetInfoManager instance = new WidgetInfoManager();
}
/**
* Privatization construction method.
*/
private WidgetInfoManager() {
}
/**
* getInstance.
*
* @return WidgetInfoManager
*/
public static WidgetInfoManager getInstance() {
return SingletonHolder.instance;
}
/**
* initializeWidgetResolveInfo.
*/
@SuppressLint("QueryPermissionsNeeded")
public void initializeWidgetResolveInfo() {
mAllWidgetResolveInfo.clear();
mAllWidgetResolveInfo.addAll(YxApplication.getContext().getPackageManager()
.queryBroadcastReceivers(new Intent(
"android.intent.action.WidgetProvider"), 0));
if (mAllWidgetResolveInfo.size() == 0) {
mHandler.postDelayed(mReloadWidgetResolveInfoRunnable, RELOAD_DELAY);
Log.i(TAG, "mAllWidgetResolveInfo is null, reload after 100ms");
} else {
mHandler.removeCallbacks(mReloadWidgetResolveInfoRunnable);
Log.i(TAG, "initializeWidgetResolveInfo: mAllWidgetResolveInfo = "
+ Arrays.toString(mAllWidgetResolveInfo.toArray()));
}
}
public void initializeWidget() {
mWidgetInfoList.addAll(DatabaseUtil.getInstance().queryAllWidgetInfos());
Log.i(TAG, "WidgetInfoManager: size = " + mWidgetInfoList.size());
}
/**
* Get AppWidgetProviderInfo by package name.
* @param pkg package name
* @return AppWidgetProviderInfo
*/
public AppWidgetProviderInfo getAppWidgetProviderInfo(String pkg) {
Log.i(TAG, "getAppWidgetProviderInfo: pkg = " + pkg);
int widgetId = -1;
AppWidgetProviderInfo appWidgetInfo;
// 1. 根據(jù)包名獲取 ComponentName
ComponentName component = getComponent(pkg);
if (component == null) {
Log.w(TAG, "getAppWidgetProviderInfo: component is null !!!");
return null;
}
// 2. 根據(jù) ComponentName 獲取已保存的 WidgetId
for (WidgetInfoEntity entity : mWidgetInfoList) {
if (component.getPackageName().equals(entity.getPackageName())
&& component.getClassName().equals(entity.getClassName())) {
widgetId = entity.getWidgetId();
break;
}
}
// 3. 判斷獲取的widgetId是否有效,如果有效就使用widgetId去拿AppWidgetProviderInfo;
//如果無效就執(zhí)行4
if (widgetId != -1) {
appWidgetInfo = YxApplication.getContext()
.getWidgetManager().getAppWidgetInfo(widgetId);
// 3.1 如果獲取的AppWidgetProviderInfo為null,則執(zhí)行4
if (appWidgetInfo == null) {
Log.w(TAG, "getAppWidgetProviderInfo: appWidgetInfo is null !!! widgetId = "
+ widgetId);
// 移除無效值
removeWidgetByPkg(component.getPackageName());
// 創(chuàng)建新的AppWidgetProviderInfo
appWidgetInfo = createAppWidgetInfo(component);
}
} else {
Log.w(TAG, "getAppWidgetProviderInfo: widgetId is -1 !!!");
// 4. 重新創(chuàng)建widgetId -> 綁定widget -> 生成新的 AppWidgetProviderInfo
// 移除無效值
removeWidgetByPkg(component.getPackageName());
// 創(chuàng)建新的 AppWidgetProviderInfo
appWidgetInfo = createAppWidgetInfo(component);
}
Log.i(TAG, "getAppWidgetProviderInfo: appWidgetInfo = " + appWidgetInfo);
return appWidgetInfo;
}
private AppWidgetProviderInfo createAppWidgetInfo(ComponentName component) {
Log.i(TAG, "createAppWidgetInfo: component = " + component.toString());
int widgetId = YxApplication.getContext().getWidgetHost().allocateAppWidgetId();
boolean isBindAppWidgetIdIfAllowed = YxApplication.getContext()
.getWidgetManager().bindAppWidgetIdIfAllowed(widgetId, component);
Log.i(TAG, "createAppWidgetInfo bindAppWidgetIdIfAllowed = "
+ isBindAppWidgetIdIfAllowed);
AppWidgetProviderInfo appWidgetInfo = YxApplication.getContext()
.getWidgetManager().getAppWidgetInfo(widgetId);
WidgetInfoEntity entity = new WidgetInfoEntity(widgetId, component.getPackageName(),
component.getClassName());
saveWidgetInfo(entity);
return appWidgetInfo;
}
private ComponentName getComponent(String pkg) {
for (ResolveInfo info : mAllWidgetResolveInfo) {
if (info.activityInfo.packageName.equals(pkg)) {
return new ComponentName(
info.activityInfo.packageName, info.activityInfo.name);
}
}
Log.w(TAG, pkg + " ComponentName is null ! "
+ " mAllWidgetResolveInfo.size = " + mAllWidgetResolveInfo.size());
return null;
}
/**
* Get widget id by pkg name.
* @param pkg package name
* @return widgetId
*/
public int getWidgetId(String pkg) {
for (WidgetInfoEntity entity : mWidgetInfoList) {
if (entity.getPackageName().equals(pkg)) {
return entity.getWidgetId();
}
}
return -1;
}
/**
* saveWidgetInfo.
*
* @param entity WidgetInfoEntity
*/
private void saveWidgetInfo(WidgetInfoEntity entity) {
Log.d(TAG, "saveWidgetInfo: entity = " + entity.toString());
// 去重,移除臟數(shù)據(jù)(入?yún)⒌膚idgetId是新生成的,可信的),保證 widgetId 的唯一性
removeDuplicateWidget(entity.getWidgetId());
mWidgetInfoList.add(entity);
DatabaseUtil.getInstance().insertWidgetInfos(entity);
}
private void removeDuplicateWidget(int widgetId) {
Iterator<WidgetInfoEntity> iterator = mWidgetInfoList.iterator();
while (iterator.hasNext()) {
WidgetInfoEntity entity = iterator.next();
if (widgetId == entity.getWidgetId()) {
iterator.remove();
DatabaseUtil.getInstance().deleteWidgetInfo(entity);
}
}
}
/**
* Remove widget by package name.
*
* @param pkg package name
*/
public void removeWidgetByPkg(String pkg) {
Iterator<WidgetInfoEntity> iterator = mWidgetInfoList.iterator();
while (iterator.hasNext()) {
WidgetInfoEntity entity = iterator.next();
if (entity.getPackageName().equals(pkg)) {
iterator.remove();
DatabaseUtil.getInstance().deleteWidgetInfo(entity);
YxApplication.getContext().getWidgetHost()
.deleteAppWidgetId(entity.getWidgetId());
break;
}
}
}
}
3.5 獲取AppWidgetHostView并顯示
我車機(jī)里有另外一個(gè)app提供了widget-provider,包名為"com.yx.mywidget",最終可以看到widget顯示在Launcher App中:
...
private void initView() {
mWidgetFrameLayout = findViewById(R.id.widget_test_fl);
mWidgetFrameLayout.addView(getWidgetView("com.yx.mywidget"));
}
/**
* Get widget view.
* @param pkg package name
* @return widget view
*/
private View getWidgetView(String pkg) {
Log.d(TAG, "getWidgetView: pkg: " + pkg);
AppWidgetProviderInfo appWidgetInfo = WidgetInfoManager.getInstance()
.getAppWidgetProviderInfo(pkg);
int widgetId = WidgetInfoManager.getInstance().getWidgetId(pkg);
Log.i(TAG, "getWidgetView: appWidgetInfo = " + appWidgetInfo
+ " widgetId = " + widgetId);
if (appWidgetInfo != null && widgetId != -1) {
AppWidgetHostView hostView = YxApplication.getContext().getWidgetHost()
.createView(YxApplication.getContext(), widgetId, appWidgetInfo);
// Remove HostView's default padding value
Log.i(TAG, "getWidgetView: pkg = " + pkg + " hostView = " + hostView);
return hostView;
}
return null;
}
...
4.總結(jié)
可以看到,想要widget顯示到Launcher上其實(shí)并不復(fù)雜,主要流程就是:文章來源:http://www.zghlxwxcb.cn/news/detail-500476.html
- 定義widgetHost并startListening
- 獲取系統(tǒng)里所有widget-provider廣播,拿到其ComponentName
- 獲取AppWidgetProviderInfo,如果首次沒有widgetId就創(chuàng)建并存儲
- 通過widgetId和AppWidgetProviderInfo獲取AppWidgetHostView并顯示
本文是我首次進(jìn)行技術(shù)性文檔的總結(jié)并發(fā)布到網(wǎng)上,感謝你的閱讀。文章來源地址http://www.zghlxwxcb.cn/news/detail-500476.html
到了這里,關(guān)于Android車載Launcher開發(fā)(1) - 顯示W(wǎng)idget的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!