Android车载Launcher开发(1) - 显示Widget

这篇具有很好参考价值的文章主要介绍了Android车载Launcher开发(1) - 显示Widget。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1.Launcher简介

Launcher是安卓系统中的桌面启动器,安卓系统的桌面UI统称为Launcher。Launcher是安卓系统中的主要程序组件之一,安卓系统中如果没有Launcher就无法启动安卓桌面。作为车机开机后用户接触到的第一个带有界面的系统级APP,和普通APP一样,它的界面也是在Activity上绘制出来的。

车机上Launcher一般分为两个界面,首页和应用列表界面。

首页一般包括用户信息、常用应用快捷方式、3D车模和widget卡片,widget卡片有:地图、天气、音乐播放器、时钟等;

Android车载Launcher开发(1) - 显示Widget

应用列表界面就是启动APP的列表界面,单击APP的Icon可进入App,长按APP的Icon可以进入编辑模式,编辑模式下APP可以进行拖拽、合并文件夹、删除等功能。

Android车载Launcher开发(1) - 显示Widget

(ps:顶部状态栏status bar和底部导航栏navigation bar属于System UI,中间才属于Launcher部分)

2.Widget概述

参考资料:应用微件概览

Widget,又称为微件或者小部件。我们可以把它当作是一个微型应用程序视图,用以嵌入到其他应用程序中(一般来说就是桌面Launcher)并接收周期性的更新。这样用户就可以方便查看应用程序的重点信息或者进行应用程序的快捷控制。

Android车载Launcher开发(1) - 显示Widget

Widget类型官方分为信息微件、集合微件、控制微件和混合微件。开发Widget是由各自应用程序(如天气、导航、音乐)开发人员开发,不是本篇的重点内容,网上有很多关于Widget开发的例子。如何使车载Launcher具有摆放Widget的能力,是我们关注的重点!

3.Launcher开发如何显示Widget

3.1 使Launcher App成为系统级App

  • Q:为什么在显示Widget的时候要把Launcher App声明为系统级的App呢?

  • A:开发Launcher App时肯定会声明其为系统级App。而显示Widget时需要App是系统级的原因是:Widget显示需要我们获取到AppWidgetManager对象并调用**public boolean bindAppWidgetIdIfAllowed(int appWidgetId, ComponentName provider)**方法,而此方法返回值要为true就需要App是系统级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、包名、类名到数据库
    WidgetInfoEntity entity = new WidgetInfoEntity(widgetId, component.getPackageName(),
            component.getClassName(), checkWidgetDisplay(component.getPackageName()));
    saveWidgetInfo(entity);
    return appWidgetInfo;
}

Launcher未声明为系统级App时截取的Log:

Android车载Launcher开发(1) - 显示Widget

将App声明为系统级App的步骤:

  1. 将车机系统签名放到项目中,创建一个keystore目录放置签名文件:

Android车载Launcher开发(1) - 显示Widget

  1. app目录下的build.gradle文件配置签名文件,在android{}内加上签名文件的配置信息,然后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”,让程序运行在系统进程中。

<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">
    //定义查询权限,查询系统中的所有widget广播需要
    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
        tools:ignore="QueryAllPackagesPermission" />
    ...
</manifest>

通过以上步骤,相当于把我们自己的开发的Launcher声明成系统级App了。

3.2 定义并初始化AppWidgetHost对象

定义类继承Application,在Application初始化的时候定义好AppWidgetHost对象并且调用**startListening()**方法

public class YxApplication extends Application {
    private static final String TAG = "Yx_YxApplication";

    private AppWidgetHost mWidgetHost;
    //自定义一个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()的回调
        mWidgetHost = new AppWidgetHost(YxApplication.getContext(), APPWIDGET_HOST_ID);
        mWidgetHost.startListening();

        //初始化数据库里存储的widget信息列表,后面会介绍数据库存储的内容
        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 数据库存储widgetId

有了我们的AppWidgetHost,我们就可以调用**allocateAppWidgetId()**方法获取widgetId,并且将其存入数据库,定义实体类WidgetInfoEntity,我用的是room数据库,存储了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层定义,将访问数据库里的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定义,数据库工具类,包含创建数据库、打开数据库、数据库操作的对外方法等:

@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

包含的内容:

  1. 查询系统里所有Widget广播的ResolveInfo列表用于获取其ComponentName
  2. 根据存储的widgetId获取或者新创建AppWidgetProviderInfo
  3. 保存新创建的widgetId到数据库,删除数据库里数据或者去重

其实,这个类最主要的目的就是拿到AppWidgetProviderInfo对象,有了这个对象才能获取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. 根据包名获取 ComponentName
        ComponentName component = getComponent(pkg);
        if (component == null) {
            Log.w(TAG, "getAppWidgetProviderInfo: component is null !!!");
            return null;
        }

        // 2. 根据 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; 
        //如果无效就执行4
        if (widgetId != -1) {
            appWidgetInfo = YxApplication.getContext()
                    .getWidgetManager().getAppWidgetInfo(widgetId);
            // 3.1 如果获取的AppWidgetProviderInfo为null,则执行4
            if (appWidgetInfo == null) {
                Log.w(TAG, "getAppWidgetProviderInfo: appWidgetInfo is null !!! widgetId = "
                        + widgetId);
                // 移除无效值
                removeWidgetByPkg(component.getPackageName());
                // 创建新的AppWidgetProviderInfo
                appWidgetInfo = createAppWidgetInfo(component);
            }
        } else {
            Log.w(TAG, "getAppWidgetProviderInfo: widgetId is -1 !!!");
            // 4. 重新创建widgetId -> 绑定widget -> 生成新的 AppWidgetProviderInfo
            // 移除无效值
            removeWidgetByPkg(component.getPackageName());
            // 创建新的 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());
        // 去重,移除脏数据(入参的widgetId是新生成的,可信的),保证 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并显示

我车机里有另外一个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;
    }
...

Android车载Launcher开发(1) - 显示Widget

4.总结

可以看到,想要widget显示到Launcher上其实并不复杂,主要流程就是:

  1. 定义widgetHost并startListening
  2. 获取系统里所有widget-provider广播,拿到其ComponentName
  3. 获取AppWidgetProviderInfo,如果首次没有widgetId就创建并存储
  4. 通过widgetId和AppWidgetProviderInfo获取AppWidgetHostView并显示

本文是我首次进行技术性文档的总结并发布到网上,感谢你的阅读。文章来源地址https://www.toymoban.com/news/detail-500476.html

到了这里,关于Android车载Launcher开发(1) - 显示Widget的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Android 车载应用开发之车载操作系统

    到 2030 年,全球电动汽车的销量将超过 7000 万辆,保有量将达到 3.8 亿辆,全球年度新车渗透率有望触及 60% 。这一数据来自国际能源署(IEA)发布的《全球电动汽车展望2023》。 市场趋势和政策努力的双加持下,新能源汽车来势凶猛,燃油车保有量逐年递减。此番景象让死去

    2024年02月22日
    浏览(49)
  • Android 13.0 Launcher3 电话和短信app图标显示未读短信和未接来电的条数

    在13.0系统产品rom定制化开发中,最近客户有需求要求在电话app图标显示未接来电的条数 在短信app图标上显示未读信息的条数 根据需求首选要在Launcher3的Launcher.java中,启动launcher时,查询未读短信和未接来电 在有未接来电时,更新未接来电的数量 在有未读短信时,更新未读短

    2024年01月17日
    浏览(78)
  • 车载Launcher中,Service下Dialog弹框,并且覆盖状态栏且状态栏不能点击

    在Service中使用系统dialog弹框,但是无法覆盖全部,底部菜单依然可以被点击,在某些场景下是不符合需求的   显然是 dialog 的层级不够高导致的,很多时候会直接修改层级,但是如果修改的层级涉及到系统权限,运行就会直接报错 getWindow().setType(WindowManager.LayoutParams.TYPE_SYS

    2024年02月09日
    浏览(37)
  • Android 车载应用开发指南(3) - SystemUI 详解

    Android 车载应用开发指南系列文章 Android 车载应用开发指南(1)- 车载操作系统全解析 Android 车载应用开发指南(2)- 应用开发入门 Android 车载应用开发指南(3)- SystemUI 详解 SystemUI 全称 System User Interface ,直译过来就是 系统级用户交互界面 ,在 Android 系统中由 SystemUI 负责

    2024年02月19日
    浏览(41)
  • Android automotive车载开发(1)-----Automotive audio

    车载音频 Android Automotive OS (AAOS) 是在核心 Android 音频堆栈的基础之上打造而成,以支持用作车辆信息娱乐系统的用例。AAOS 负责实现信息娱乐声音(即媒体、导航和通讯声音),但不直接负责具有严格可用性和计时要求的铃声和警告。虽然 AAOS 提供了信号和机制来帮助车辆管

    2023年04月08日
    浏览(35)
  • android车载开发,如何模拟器上实现多屏

    三个点,Display-addSecondary display   方案一 通过Presentation来实现,他是一个Dialog(context,display) 方案二 使用ActivityOptions设置activity显示在哪个屏幕上 launchDisplayId 方案三 adb shell am start -n 包名+Activity

    2024年02月12日
    浏览(47)
  • 车载Android应用开发与分析 - 初试 SystemUI Plugin

    在前面的视频、文章中我们介绍完了整个车载Android应用开发所需要的基础知识: 【视频文稿】车载Android应用开发与分析 - 走进车载操作系统 - 掘金 【视频文稿】车载Android应用开发与分析 - AOSP的下载与编译 - 掘金 【视频文稿】车载Android应用开发与分析 - 开发系统应用 - 掘

    2024年02月02日
    浏览(38)
  • android多屏触摸相关的详解方案-安卓framework开发手机车载车机系统开发课程

    直播免费视频课程地址:https://www.bilibili.com/video/BV1hN4y1R7t2/ 在做双屏相关需求开发过程中,经常会有对两个屏幕都要求可以正确触摸的场景。但是目前我们模拟器默认创建的双屏其实是没有办法进行触摸的 静态修改方案 使用命令查看display2即副屏的信息情况 adb shell dumpsys d

    2024年02月11日
    浏览(47)
  • 第二节-安卓多屏双屏实战车载车机智能驾驶舱开发/千里马android framwork开发

    hi,粉丝朋友们! 上一节已经对车载的多屏互动进行了相关的技术方案介绍,以及相关的核心方法 moveRootTaskToDisplay的讲解和使用。 具体可以参考链接:https://blog.csdn.net/learnframework/article/details/130461689 本节就来进行代码实战 要实现双屏互动,主要就只需要两个步骤: 1、手指动

    2024年02月09日
    浏览(49)
  • Android 11 系统开发增加低电量弹窗提示 手机 平板 车载 TV 投影 通用

    SystemUIService 中启动PowerUI 主要修改 5、在symbols 文件中添加对应java-symbol方便Framework代码引用code

    2024年03月15日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包