AppWidget(桌面小部件)

2022-06-01  本文已影响0人  眼中有码

一、引言

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

二、AppWidget简介

如下图红色箭头所指的都是 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

<?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>

其中

四、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" />
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())。

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
上一篇下一篇

猜你喜欢

热点阅读