美文网首页
AppWidget(桌面小部件)

AppWidget(桌面小部件)

作者: 眼中有码 | 来源:发表于2022-06-01 10:37 被阅读0次

    一、引言

    最近开始准备做车机的 Launcher ,之前没接触过Launcher最近开始恶补这块知识,在学习Launcher3 的过程中发现了一个很有趣的东西那就 AppWidget(桌面小部件),
    并且在我们项目规划的Launcher 中AppWidget占了很大的比重,所以学习好AppWidget至关重要。

    二、AppWidget简介

    • Android widget 也称为桌面插件,其是android系统应用开发层面的一部分,但是又有特殊用途,而且会成为整个android系统的亮点。Android中的AppWidget与google widget和中移动的widget并不是一个概念,这里的AppWidget只是把一个进程的控件嵌入到别外一个进程的窗口里的一种方法。
    • AppWidget的服务核心在AppWidgetService中,它是系统应用,在SystemServer进程中。
    • AppWidget的提供方由应用提供(对大部分应用开发者来说,了解操作这一块就够了)。
    • AppWidget的显示方,基本上运行在Launcher中。
    • AppWidget支持的控件是由局限性的,比如不支持RecyclerView等。
    • RemoteViews 在Android中的使用场景主要有:自定义通知栏和桌面小部件。

    如下图红色箭头所指的都是 AppWidget


    image.png

    三、Launcher3 AppWidget的启动添加流程

    1. Launcher3启动添加

    Launcher启动onCreate()方法初始化mAppWidgetManager, mAppWidgetHost对象,AppWidgetHost是launcher承载AppWidgetView的宿主。

    public void onCreate() {
        ...
        //得到AppWidget管理实例 : AppWidgetManager , AppWidgetHost , AppWidgetHostView三个类的关系
        mAppWidgetManager = AppWidgetManagerCompat.getInstance(this);  //1
        mAppWidgetHost = new LauncherAppWidgetHost(this);  //2
        // Host启动监听,监听LauncherProvider中的数据改变
        mAppWidgetHost.startListening();  //3
        ...
    }
    
    1. AppWidgetManagerCompat 管理类是一个单例模式的兼容类
        public static AppWidgetManagerCompat getInstance(Context context) {
            synchronized (sInstanceLock) {
                if (sInstance == null) {
                    if (Utilities.ATLEAST_OREO) {
                        sInstance = new AppWidgetManagerCompatVO(context.getApplicationContext());
                    } else {
                        sInstance = new AppWidgetManagerCompatVL(context.getApplicationContext());
                    }
                }
                return sInstance;
            }
        }
    
    1. LauncherAppWidgetHost extends AppWidgetHost 由其父类完成初始化对象,创建用于回调的Callbacks服务类IAppWidgetHost.Stub, 绑定服务bindService,得到IAppWidgetService对象,进行launcher和AppWidgetService之间的调用
     public AppWidgetHost(Context context, int hostId, OnClickHandler handler, Looper looper) {
            mContextOpPackageName = context.getOpPackageName();
            mHostId = hostId;
            mOnClickHandler = handler;
            mHandler = new UpdateHandler(looper);
            mCallbacks = new Callbacks(mHandler);
            mDisplayMetrics = context.getResources().getDisplayMetrics();
            bindService(context);
        }
    
        private static void bindService(Context context) {
            synchronized (sServiceLock) {
                if (sServiceInitialized) {
                    return;
                }
                sServiceInitialized = true;
                PackageManager packageManager = context.getPackageManager();
                if (!packageManager.hasSystemFeature(PackageManager.FEATURE_APP_WIDGETS)
                        && !context.getResources().getBoolean(R.bool.config_enableAppWidgetService)) {
                    return;
                }
                IBinder b = ServiceManager.getService(Context.APPWIDGET_SERVICE);
                sService = IAppWidgetService.Stub.asInterface(b);
            }
        }
    
    1. 在startListening 方法中 ,通过IAppWidgetService.startListening 方法解析Launcher中的AppWidget信息保存到系统服务成员变量中。
      public void startListening() {
            if (sService == null) {
                return;
            }
            final int[] idsToUpdate;
            synchronized (mViews) {
                int N = mViews.size();
                idsToUpdate = new int[N];
                for (int i = 0; i < N; i++) {
                    idsToUpdate[i] = mViews.keyAt(i);
                }
            }
            List<PendingHostUpdate> updates;
            try {
                updates = sService.startListening(
                        mCallbacks, mContextOpPackageName, mHostId, idsToUpdate).getList();
            }
            catch (RemoteException e) {
                throw new RuntimeException("system server dead?", e);
            }
    
            int N = updates.size();
            for (int i = 0; i < N; i++) {
                PendingHostUpdate update = updates.get(i);
                switch (update.type) {
                    case PendingHostUpdate.TYPE_VIEWS_UPDATE:
                        updateAppWidgetView(update.appWidgetId, update.views);
                        break;
                    case PendingHostUpdate.TYPE_PROVIDER_CHANGED:
                        onProviderChanged(update.appWidgetId, update.widgetInfo);
                        break;
                    case PendingHostUpdate.TYPE_VIEW_DATA_CHANGED:
                        viewDataChanged(update.appWidgetId, update.viewId);
                }
            }
        }
    
    1. 当添加AppWidget时,首页返回到Launcher中的onActivityResult中,在handleActivityResult中创建添加小部件意图,之后返回到onActivityForResult,调用completeAddAppWidget,通过IAppWidgetService.getAppWidgetInfo,获取AppWidgetProviderInfo,保存到本地数据库中addItemToDatabase(),并创建AppWidgetHostView 对象,mAppWidgetHost.createView,返回RemoteView对象,IAppWidgetService。getAppWidgetViews(),调用AppWidgetHostView.updateAppWidget(views);更新View到launcher界面上mWorkspace.addInScreen(hostView, launcherInfo);
      @Thunk void completeAddAppWidget(int appWidgetId, ItemInfo itemInfo,
                AppWidgetHostView hostView, LauncherAppWidgetProviderInfo appWidgetInfo) {
    
            if (appWidgetInfo == null) {
                appWidgetInfo = mAppWidgetManager.getLauncherAppWidgetInfo(appWidgetId);
            }
    
            LauncherAppWidgetInfo launcherInfo;
            launcherInfo = new LauncherAppWidgetInfo(appWidgetId, appWidgetInfo.provider);
            launcherInfo.spanX = itemInfo.spanX;
            launcherInfo.spanY = itemInfo.spanY;
            launcherInfo.minSpanX = itemInfo.minSpanX;
            launcherInfo.minSpanY = itemInfo.minSpanY;
            launcherInfo.user = appWidgetInfo.getProfile();
    
            getModelWriter().addItemToDatabase(launcherInfo,
                    itemInfo.container, itemInfo.screenId, itemInfo.cellX, itemInfo.cellY);
    
            if (hostView == null) {
                // Perform actual inflation because we're live
                hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo);
            }
            hostView.setVisibility(View.VISIBLE);
            prepareAppWidget(hostView, launcherInfo);
            mWorkspace.addInScreen(hostView, launcherInfo);
        }
    
    1. 当AppWidgetProvider获得更新的广播,并执行onUpdate(),onUpdate()中创建了RemoteViews并通过AppWidgetManager.updateAppWidget()更新到AppWidgetService之后,AppWidgetService会通过注册的IAppWidgetHost的回调,执行AppWidgetHost的更新。

    2. Lancher3 预置 AppWidget

    • 添加权限
      <uses-permission android:name="android.permission.BIND_APPWIDGET"" />
    • 在res/xml/default_workspace_4x4.xml、default_workspace_5x5.xml等中添加
    <?xml version="1.0" encoding="utf-8"?>
    <!-- Copyright (C) 2009 The Android Open Source Project
    
         Licensed under the Apache License, Version 2.0 (the "License");
         you may not use this file except in compliance with the License.
         You may obtain a copy of the License at
    
              http://www.apache.org/licenses/LICENSE-2.0
    
         Unless required by applicable law or agreed to in writing, software
         distributed under the License is distributed on an "AS IS" BASIS,
         WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
         See the License for the specific language governing permissions and
         limitations under the License.
    -->
    
    <favorites xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3">
    
        <!-- Hotseat -->
        <include launcher:workspace="@xml/dw_phone_hotseat" />
    
        <!-- Bottom row -->
        <resolve
            launcher:screen="0"
            launcher:x="0"
            launcher:y="-1" >
            <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_EMAIL;end" />
            <favorite launcher:uri="mailto:" />
    
        </resolve>
    
        <resolve
            launcher:screen="0"
            launcher:x="1"
            launcher:y="-1" >
            <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_GALLERY;end" />
            <favorite launcher:uri="#Intent;type=images/*;end" />
    
        </resolve>
    
        <resolve
            launcher:screen="0"
            launcher:x="4"
            launcher:y="-1" >
            <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_MARKET;end" />
            <favorite launcher:uri="market://details?id=com.android.launcher" />
        </resolve>
    
         <!--  预置 小组件 -->
        <appwidget
            launcher:packageName="com.example.democollect"
            launcher:className="com.example.democollect.appwidget.MyAppWidgetProvider"
            launcher:screen="0"
            launcher:container="-100"
            launcher:spanX="3"
            launcher:spanY="1"
            launcher:x="2"
            launcher:y="2"/>
    
    </favorites>
    
    

    其中

    • launcher:container="-100",表示添加在 desktop 中,如果是-101那就是在 HotSeat 中,但是这里我们的 widget 是要添加在 desktop,所以是-100;
    • launcher:packageName=“com.android.deskclock”,这个没啥说的,就是widget的包名,我这里添加的是数字时钟,所以这里填写的是 时钟模块 的包名;
    • launcher:className=“com.android.alarmclock.DigitalAppWidgetProvider”,这个是 widget 所在的类,这是是数字时钟,如果要添加 表盘时钟(指针时钟),就填写com.android.alarmclock.AnalogAppWidgetProvider;
    • launcher:screen=“0”,这个是添加在哪一屏;
    • launcher:spanX=“5”,这个表示 widget 在 x 方向上占位多少,我的launcher是 x 方向可以放5个APP图标,所以这里widget是占满整个 x 方向;
    • launcher:spanY=“2”,这个表示 widget 在 y 方向上站位多少,2表示占用相当于两个APP图标的高度;
    • launcher:x=“0”,这个表示 widget 的 x 方向上的位置,这里0表示从屏幕最左侧开始显示;
    • launcher:y=“2”,这个表示 widget 的 y 方向上的位置,这里3表示从上往下第3个位置开始显示(从0开始,所以2就是第3个)。

    四、AppWidget的使用

    1. 大致思路:

    1. 在AndroidManifest中声明AppWidget。
    2. 在xml目录中定义AppWidget的配置文件。
    3. 在layout目录中定义Widget的布局文件。
    4. 新建一个类,继承AppWidgetProvider类,实现具体的widget业务逻辑

    2. 具体使用步骤:

    1. 在 AndroidManifest 中声明 App Widget
      <receiver
                android:name=".appwidget.MyAppWidgetProvider"
                android:label="测试小组件">
                <intent-filter>
                   <!--所有的窗口小部件都接收android.appwidget.action.APPWIDGET_UPDATE 动作的广播,
                    该广播根据android:updatePeriodMillis设定的间隔时间发出广播,用于定时更新桌面上的所有窗口小部件。-->
                    <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
                    <!--定义一个自定义的动作广播,可以通过在该广播接收器中注册自定义的动作以使窗口小部件接收自定义的广播。-->
                    <action android:name="com.oitsme.REFRESH_WIDGET" />
                    <action android:name="com.oitsme.LOCK_ACTION" />
                    <action android:name="com.oitsme.UNLOCK_ACTION" />
                </intent-filter>
                 <!--声明了 Widget 的 AppWidgetProviderInfo 对应的资源 xml 的位置,用的是 xml 目录下的 example_appwidget_info.xml。-->
                <meta-data
                    android:name="android.appwidget.provider"
                    android:resource="@xml/appwidget" />
            </receiver>
    
    2. 在 xml 目录定义 App Widget 的初始化 xml 文件
    <?xml version="1.0" encoding="utf-8"?>
    <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
        android:initialLayout="@layout/appwidget_layout"
        android:minWidth="200dp"
        android:minHeight="100dp"
        android:previewImage="@mipmap/ic_launcher"
        android:resizeMode="vertical|horizontal"
        android:updatePeriodMillis="0"
        android:widgetCategory="home_screen|keyguard" />
    
    • minWidth & minHeight:定义了 Widget 的最小宽高,当 minWidth 和 minHeight 不是桌面 cell 的整数倍时,Widget 的宽高会被阔至与其最接近的 cells 大小。Google 官方给出了一个大致估算 minWidth & minHeight 的公式,根据 Widget 所占的 cell 数量来计算宽高:70 × n − 30,n 是所占的 cell 数量。
    • updatePeriodMillis:定义了 Widget 的刷新频率,也就是 App Widget Framework 多久请求一次 AppWidgetProvider 的 onUpdate() 回调函数。该时间间隔并不保证精确,出于节约用户电量的考虑,Android 系统默认最小更新周期是 30 分钟,也就是说:如果您的程序需要实时更新数据,设置这个更新周期是 2 秒,那么您的程序是不会每隔 2 秒就收到更新通知的,而是要等到 30 分钟以上才可以,要想实时的更新 Widget,一般可以采用 Service 和 AlarmManager 对 Widget 进行更新。
    • previewImage:当用户选择添加 Widget 时的预览图片。如果该属性没有定义,则展示 application 的 launcher icon。该属性是在 3.0 以后引入的。
    • initialLayout:Widget 的布局 Layout 文件。
    • configure:定义了用户在添加 Widget 时弹出的配置页面的 Activity,用户可以在此进行 Widget 的一些配置,该 Activity 是可选的,如果不需要可以不进行声明。
    • resizeMode:Widget 在水平和垂直方向是否可以调整大小,值可以为:horizontal(水平方向可以调整大小),vertical(垂直方向可以调整大小),none(不可以调整大小),也可以 horizontal|vertical 组合表示水平和垂直方向均可以调整大小。
    • widgetCategory:表示 Widget 可以显示的位置,包括 home_screen(桌面),keyguard(锁屏),keyguard 属性需要 5.0 或以上 Android 版本才可以。
    3. layout文件布局
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/ll_right"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
    
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:background="#ccc">
    
            <ImageView
                android:id="@+id/iv_icon"
                android:layout_width="30dp"
                android:layout_height="30dp"
                android:layout_centerVertical="true"
                android:layout_marginEnd="5dp"
                android:layout_marginStart="5dp"
                android:background="@mipmap/ic_launcher_round" />
    
            <TextView
                android:id="@+id/tv_title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:layout_toEndOf="@id/iv_icon"
                android:text="Widget" />
    
            <LinearLayout
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_alignParentEnd="true"
                android:gravity="center_vertical"
                android:orientation="horizontal">
    
                <ProgressBar
                    android:id="@+id/progress_bar"
                    android:layout_width="20dp"
                    android:layout_height="20dp"
                    android:indeterminateTint="@color/teal_200"
                    android:indeterminateTintMode="src_atop"
                    android:visibility="gone" />
    
                <Button
                    android:id="@+id/tv_refresh"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginEnd="15dp"
                    android:text="刷新"
                    android:padding="5dp"
                    android:textSize="12sp" />
            </LinearLayout>
    
        </RelativeLayout>
    </LinearLayout>
    

    仅支持以下布局类:
    FrameLayout、LinearLayout 、RelativeLayout 、GridLayout 、AnalogClock 、Button 、Chronometer 、ImageButton 、ImageView 、ProgressBar 、TextView 、ViewFlipper 、 ListView 、 GridView 、StackView 、AdapterViewFlipper 、 ViewStub 不支持这些类的后代。

    4. 自定义一个类 继承 AppWidgetProvider 类

    AppWidgetProvider 继承自 BroadcastReceiver,内部逻辑非常简单,就是在 onReceive() 中处理 Widget 相关的广播事件,分发到各个回调函数中(onUpdate(), onDeleted(), onEnabled(), onDisabled, onAppWidgetOptionsChanged())。

    • onUpdate():是最重要的回调函数,根据 updatePeriodMillis 定义的定期刷新操作会调用该函数,此外当用户添加 Widget 时 也会调用该函数,可以在这里进行必要的初始化操作。但如果在<appwidget-provider>中声明了 android:configure 的 Activity,在用户添加 Widget 时,不会调用 onUpdate(),需要由 configure Activity 去负责去调用 AppWidgetManager.updateAppWidget() 完成 Widget 更新,后续的定时更新还是会继续调用 onUpdate() 的。
    • onDeleted():当 Widget 被删除时调用该方法。
    • onEnabled():当 Widget 第一次被添加时调用,例如用户添加了两个你的 Widget,那么只有在添加第一个 Widget 时该方法会被调用。所以该方法比较适合执行你所有 Widgets 只需进行一次的操作。
    • onDisabled():与 onEnabled 恰好相反,当你的最后一个 Widget 被删除时调用该方法,所以这里用来清理之前在 onEnabled() 中进行的操作。
    • onAppWidgetOptionsChanged():当 Widget 第一次被添加或者大小发生变化时调用该方法,可以在此控制 Widget 元素的显示和隐藏。
    public class MyAppWidgetProvider extends AppWidgetProvider {
    
        private static final String TAG = MyAppWidgetProvider.class.getSimpleName();
        public static final String REFRESH_WIDGET = "com.oitsme.REFRESH_WIDGET";
        private Context mContext;
    
        private static final Handler mHandler = new Handler();
        private final Runnable runnable = new Runnable() {
            @Override
            public void run() {
                hideLoading(mContext);
            }
        };
        @Override
        public void onReceive(Context context, Intent intent) {
            super.onReceive(context, intent);
            String action = intent.getAction();
            Log.i(TAG, "onReceive");
            if (action.equals(REFRESH_WIDGET)) {
                // 接受“bt_refresh”的点击事件的广播
                showLoading(context);
                mHandler.postDelayed(runnable, 2000);
            }
        }
    
        /**
         * 到达指定的更新时间或者当用户向桌面添加AppWidget时被调用
         * appWidgetIds:桌面上所有的widget都会被分配一个唯一的ID标识,这个数组就是他们的列表
         *
         * @param context
         * @param appWidgetManager
         * @param appWidgetIds
         */
        @Override
      @Override
        public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
            super.onUpdate(context, appWidgetManager, appWidgetIds);
            this.mContext = context;
            Log.i(TAG, "onUpdate");
            // 获取AppWidget对应的视图
            RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.appwidget_layout);
            // 设置响应 “按钮(bt_refresh)” 的intent
            Intent btIntent = new Intent(context, MyAppWidgetProvider.class);
            btIntent.setAction(REFRESH_WIDGET);
    //            btIntent.putExtra(REFRESH_WIDGET,"REFRESH_WIDGET");
            PendingIntent btPendingIntent = PendingIntent.getBroadcast(context, 0, btIntent, PendingIntent.FLAG_UPDATE_CURRENT);
            remoteViews.setOnClickPendingIntent(R.id.tv_refresh, btPendingIntent);
            // 调用集合管理器对集合进行更新
            appWidgetManager.updateAppWidget(appWidgetIds, remoteViews);
        }
    
        /**
         * 显示加载loading
         */
        private void showLoading(Context context) {
            RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.appwidget_layout);
            remoteViews.setViewVisibility(R.id.tv_refresh, View.VISIBLE);
            remoteViews.setViewVisibility(R.id.progress_bar, View.VISIBLE);
            remoteViews.setTextViewText(R.id.tv_refresh, "正在刷新...");
            refreshWidget(context, remoteViews, false);
        }
        /**
         * 隐藏加载loading
         */
        private void hideLoading(Context context) {
            RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.appwidget_layout);
            remoteViews.setViewVisibility(R.id.progress_bar, View.GONE);
            remoteViews.setTextViewText(R.id.tv_refresh, "刷新");
            refreshWidget(context, remoteViews, false);
        }
        /**
         * 刷新Widget
         */
        private void refreshWidget(Context context, RemoteViews remoteViews, boolean refreshList) {
            AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
            ComponentName componentName = new ComponentName(context, MyAppWidgetProvider.class);
            appWidgetManager.updateAppWidget(componentName, remoteViews);
        }
    }
    
    
    1. onUpdate()方法中首先需要new一个RemoteViews,构造方法里需要传递两个参数,一个是包名(context.getPacakgeName),一个是布局文件(layout_widget)。
      然后通过remoteViews.setOnClickPendingIntent()设置按钮的点击事件。setOnClickPendingIntent()中需要传递两个参数:一个是id(比如需要被点击的button),一个是PendingIntent。PendingIntent是未来的意图。
      于是我们需要事先构造一个PendingIntent,这个需要通过 PendingIntent.getBroadcast()来构造。getBroadcast()方法中需要传递四个参数,其中有一个是Intent。于是我们需要构造一个Intent。在intent里发送广播,并设置Action。按钮点击完了之后,记得调用appWidgetManager.updateAppWidget(int[] appWidgetIds, RemoteViews views)方法更新一下,第一个参数就是onUpdate方法中的参数,代表的是所有的控件。在onUpdate()方法中通过intent发送按钮点击时间的广播之后,我们需要在onReceive()方法中进行广播的接收。
    2. onReceive()方法中当intent的action匹配成功时,开始执行做点击时间之后的setText,不过这里需要重新new 一个 RemoteViews,而不能共用onUpdate()方法中的RemoteViews(这是一个很大的坑)。执行完点击事件之后的setText之后,记得调用appWidgetManager.updateAppWidget(ComponentName, RemoteViews)方法,第一个参数为组件名,需要我们自己new一下,第二个参数很好解释。
    5. 如何显示在桌面
    1. 桌面长按桌面空白部分弹框选择 Widgets


      image.png
    2. 选择自己的小组件长按拖拽到桌面


      image.png
      image.png

    相关文章

      网友评论

          本文标题:AppWidget(桌面小部件)

          本文链接:https://www.haomeiwen.com/subject/hufxmrtx.html