Android Widget 基础介绍以及常见问题

本文是 Android Widget(小部件) 系列的第一篇,主要是对 Android widget (小部件)基本原理、开发流程、以及常见问题做了简单的介绍。
本系列的目的是通过对Android 小部件的梳理,了解小部件刷新流程、恢复流程、以及系统发生变化时,小部件是如何适配的,解决在开发小部件过程中遇到的问题。系列文章大部份来自源码的解读,内容非常多,也非常容易遗忘,因此记录分享。

系列文章
Android Widget 基础介绍以及常见问题
安卓小部件刷新源码解析一非列表
安卓小部件(APPWidget)刷新源码解析一列表

一、Android Widget 原理常见问题

1、小部件是什么?

image.png

App widgets are miniature application views that can be embedded in other applications (such as the home screen) and receive periodic updates。
通俗解释:一个能够定期刷新并且加到其他应用上的微型视图。
官网

2、小部件的运行机制是什么?

image.png
  • 通过 AppWidgetProvider 定义小部件的行为
  • 通过 RemoteView 和布局文件定义小部件的UI
  • 通过AppWidgetManager 更新视图
  • 在manifeset 里注册 AppWidgetProvider(继承于广播),设置监听的action以及配置文件

3、RemoteView如何工作?

RemoteView 继承于Parcelable,可在进程间传递。RemoteView 会将每一个设置的行为转换成相应的Action。在Host 测时再将Action 翻译成对应的行为。

4、小部件运行在什么进程?

小部件的运行逻辑需要分为三部分:AppWidgetProvider 中的逻辑运行在小部件所在应用进程。小部件查找以及权限校验的逻辑运行在system_process中。小部件渲染逻辑在host 进程中。

二、开发中常见问题

1、开发一个小部件有哪必要流程?

  1. 新建一个类继承AppWidgetProvider用于定义主要的逻辑和行为
public class ExampleAppWidgetProvider extends AppWidgetProvider {
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
     // 更新逻辑
     }
}
  1. 新建一个配置文件描述AppWidgetProviderInfo 信息
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
        android:minWidth="40dp" // 最小宽,用于计算横向网格数
        android:minHeight="40dp" // 最小高,用于计算纵向网格数
        android:updatePeriodMillis="86400000" // 刷新时间间隔,最小为30min
        android:previewImage="@drawable/preview" //定义预览图片
        android:initialLayout="@layout/example_appwidget" 定义初始化布局,remoteView 布局未加载结束前视图
        android:configure="com.example.android.ExampleAppWidgetConfigure" //定义设置页
        android:resizeMode="horizontal|vertical" //定义尺寸模式
        android:widgetCategory="home_screen">  //定义种类,有桌面、锁屏、输入法
    </appwidget-provider>
  1. 在AndroidManifest.xml 中注册
<receiver android:name="ExampleAppWidgetProvider" >
         // 监听更新的acion
        <intent-filter>
            <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
        </intent-filter>
        <meta-data android:name="android.appwidget.provider"
                   android:resource="@xml/example_appwidget_info" />
</receiver>

2、如何设置minWidth 和 minHeight

minWidth 和 minHeight 主要用于计算横向和纵向所占格子数,不通厂商计算方式不同,但大概率都会符合谷歌规范规范

image.png

  • 4*2 横向范围 250~320 纵向是110~180
  • 2*2 横向范围110~180 纵向是110~180

3、如何AppWidgetProvider 如何更新小部件?

// appWidgetManager和widgetId 从 onUpdate 方法中获取
RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
          R.layout.xxx);
appWidgetManager.updateAppWidget(widgetId, remoteViews);

4、应用里如何更新小部件?

RemoteViews remoteViews = new RemoteViews(context.getPackageName(),R.layout.xxx);
AppWidgetManager appWidgetManager = AppWidgetManager.*getInstance*(context);
// NormalExampleWidgetProvider 为小部件组件名字,这里仅示例
ComponentName componentName = new ComponentName(context, NormalExampleWidgetProvider.class);
appWidgetManager.updateAppWidget(componentName, remoteViews);

5、如何设置点击事件?

// 生成PendingIntent
Intent intent = new Intent(context, ExampleActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
//生成 RemoteViews 关联 PendingIntent
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget_provider_layout);
views.setOnClickPendingIntent(R.id.button, pendingIntent);
// 关联 widget 和 RemoteViews
appWidgetManager.updateAppWidget(appWidgetId, views);

6、Widget 中 List 设置了setRemoteAdapter,第二次添加该小部件时,为什么没有调用onGetViewFactory ?

原因可能是RemoteViewsAdapter 复用,系统认为没有数据改变,导致没有回调onGetViewFactory,这个在google demo 也有说明。

  1. 原因分析
class AbsListView {
    public void setRemoteViewsAdapter(Intent intent, boolean isAsync) {
    // Ensure that we don't already have a RemoteViewsAdapter that is bound to an existing
    // service handling the specified intent.
    if (mRemoteAdapter != null) {
        Intent.FilterComparison fcNew = new Intent.FilterComparison(intent);
        Intent.FilterComparison fcOld = new Intent.FilterComparison(
                mRemoteAdapter.getRemoteViewsServiceIntent());
        // 比较两个是否        
        if (fcNew.equals(fcOld)) {
            return;
        }
    } 
    mDeferNotifyDataSetChanged = false;
    // Otherwise, create a new RemoteViewsAdapter for binding
    mRemoteAdapter = new RemoteViewsAdapter(getContext(), intent, this, isAsync);
    if (mRemoteAdapter.isDataReady()) {
        setAdapter(mRemoteAdapter);
    }
}
}
class Intent {   
    public boolean equals(@Nullable Object obj) {
        if (obj instanceof FilterComparison) {
            Intent other = ((FilterComparison)obj).mIntent;
            return mIntent.filterEquals(other);
        }
        return false;
    }
    public boolean filterEquals(Intent other) {
        if (other == null) {
            return false;
        }
        if (!Objects.equals(this.mAction, other.mAction)) return false;
        if (!Objects.equals(this.mData, other.mData)) return false;
        if (!Objects.equals(this.mType, other.mType)) return false;
        if (!Objects.equals(this.mIdentifier, other.mIdentifier)) return false;
        if (!(this.hasPackageEquivalentComponent() && other.hasPackageEquivalentComponent())
                && !Objects.equals(this.mPackage, other.mPackage)) {
            return false;
        }
        if (!Objects.equals(this.mComponent, other.mComponent)) return false;
        if (!Objects.equals(this.mCategories, other.mCategories)) return false;
        return true;
    }
}
  1. 解决方案
// Here we setup the intent which points to the StackViewService which will
// provide the views for this collection.
Intent intent = new Intent(context, StackWidgetService.class);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
// When intents are compared, the extras are ignored, so we need to embed the extras
// into the data so that the extras will not be ignored.
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
rv.setRemoteAdapter(appWidgetIds[i], R.id.stack_view, intent);
https://android.googlesource.com/platform/development/+/master/samples/StackWidget/src/com/example/android/stackwidget/StackWidgetProvider.java

到这里,Android Widget 基本使用以及常见问题就已经说完了。但使用中你可能会遇到各种各样的问题,而要解决问题,就需要你对相应的流程熟悉。因此才会有这一些列的文章。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容