AppWidget(桌面小部件)

一、引言

作为一个车机的Launcher开发,总结了下自己认为的难点技术:ApppWidget、拖拽、RemoteAnimation(应用打开/关闭动画)。今天就其中的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 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

  • 添加权限
  • 在res/xml/default_workspace_4x4.xml、default_workspace_5x5.xml等中添加





    
    

    
    
        
        

    

    
        
        

    

    
        
        
    

     
    



其中

  • 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
  
            
               
                
                
                
                
                
            
             
            
        
2. 在 xml 目录定义 App Widget 的初始化 xml 文件


  • 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文件布局


    

        

        

        

            

            

仅支持以下布局类:
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 时 也会调用该函数,可以在这里进行必要的初始化操作。但如果在中声明了 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(桌面小部件))