RemoteViews原理分析及应用

Posted aspook

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RemoteViews原理分析及应用相关的知识,希望对你有一定的参考价值。

转载请注明出处:http://blog.csdn.net/ahence/article/details/62418926

RemoteViews基本概念

RemoteViews乍一看名字似乎也是一种View,实则不然,它并不是View。来看RemoteViews的定义及官方说明:

/**
 * A class that describes a view hierarchy that can be displayed in
 * another process. The hierarchy is inflated from a layout resource
 * file, and this class provides some basic operations for modifying
 * the content of the inflated hierarchy.
 */
public class RemoteViews implements Parcelable, Filter {
    ……
}

我们可以得到以下几点结论:

  • RemoteViews只是一个实现了Parcelable和Filter接口的类,而并非继承自View。
  • RemoteViews用来描述可运行在其他进程中的视图结构,但RemoteViews本身不是视图,只是一个描述类。
  • RemoteViews描述的远程视图需要通过layout资源文件定义。
  • RemoteViews类提供了一系列修改远程视图的方法。

现在我们对RemoteViews应该有了一个大概的认识,它可以跨进程来显示和更新视图。RemoteViews主要有两个应用场景:

  • 自定义通知栏
  • 桌面小部件

本文最后会给出RemoteViews的应用实例,接下来我们先分析RemoteViews的实现原理。

RemoteViews原理分析

构造函数

RemoteViews提供了多个构造函数,如:

public RemoteViews(String packageName, int layoutId) {}
public RemoteViews(String packageName, int userId, int layoutId) {}
public RemoteViews(RemoteViews landscape, RemoteViews portrait) {}
public RemoteViews(Parcel parcel) {}

以一个最常用的构造方法为例:

/**
 * Create a new RemoteViews object that will display the views contained
 * in the specified layout file.
 *
 * @param packageName Name of the package that contains the layout resource
 * @param layoutId The id of the layout resource
 */
public RemoteViews(String packageName, int layoutId) {
    this(getApplicationInfo(packageName, UserHandle.myUserId()), layoutId);
}

由注释可知,第一个参数为包名,第二个参数为布局资源文件的ID,接下来会调用:

/**
 * Create a new RemoteViews object that will display the views contained
 * in the specified layout file.
 *
 * @param application The application whose content is shown by the views.
 * @param layoutId The id of the layout resource.
 *
 * @hide
 */
protected RemoteViews(ApplicationInfo application, int layoutId) {
    mApplication = application;
    mLayoutId = layoutId;
    mBitmapCache = new BitmapCache();
    // setup the memory usage statistics
    mMemoryUsageCounter = new MemoryUsageCounter();
    recalculateMemoryUsage();
}

同样也很简单,第一个参数为远程视图展示内容所属的Application信息,第二个参数为布局文件ID。该构造方法主要是初始化mApplicationmLayoutId,其他代码为图片缓存及内存计算的一些逻辑。

核心属性字段

RemoteView有如下几个比较重要的属性字段:

/**
 * Application that hosts the remote views.
 *
 * @hide
 */
private ApplicationInfo mApplication;

/**
 * The resource ID of the layout file. (Added to the parcel)
 */
private final int mLayoutId;

/**
 * An array of actions to perform on the view tree once it has been
 * inflated
 */
private ArrayList<Action> mActions;

前两个比较好理解,而且上文提到是在构造函数中赋值的。mActions是用来存储Action的一个列表,而Action可以理解为对远程视图操作的一个封装,下文会详细解释。

RemoteView注解

在RemoteViews源码中声明了如下注解:

/**
 * This annotation indicates that a subclass of View is alllowed to be used
 * with the {@link RemoteViews} mechanism.
 */
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface RemoteView {
}

从注解类型来看为运行时注解,作用于类或接口,结合注释可知此注解用于View的子类,用来标识该View是否可以作为远程视图使用。由此我们也可以推断,并非所有View都可以作为远程视图,只有声明了RemoteView注解的View才可以。我们从源码定义来简单验证一下:

TextView的定义

@RemoteView
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {}

Button的定义

@RemoteView
public class Button extends TextView {}

ImageView的定义

@RemoteView
public class ImageView extends View {}

ProgressBar的定义

@RemoteView
public class ProgressBar extends View {}

LinearLayout的定义

@RemoteView
public class LinearLayout extends ViewGroup {}

EditText的定义

public class EditText extends TextView {}

不再一一列举,可见EditText虽然是继承自TextView的,但它没有使用@RemoteView注解,因此并不能用作远程视图。

RemoteViews所支持的View类型如下:

LinearLayout、RelativeLayout、FrameLayout、GridLayout、AbsoluteLayout(已弃用)

TextView、Button、ImageView、ImageButton、Chronometer、ProgressBar、ListView、GridView、StackView、ViewFlipper、AdapterViewFlipper、ViewStub、AnalogClock(已弃用)

也就是说远程视图只能使用上述所列举的View,它们的子类及其他View都是不支持的,如果使用了不支持的View,则会报异常。

实现Parcelable和Filter接口的意义

Parcelable比较容易理解,就是支持序列化以便于跨进程操作。

那么Filter的作用是什么呢?Filter接口的定义如下:

/**
 * Hook to allow clients of the LayoutInflater to restrict the set of Views that are allowed
 * to be inflated.
 * 
 */
public interface Filter {
    /**
     * Hook to allow clients of the LayoutInflater to restrict the set of Views 
     * that are allowed to be inflated.
     * 
     * @param clazz The class object for the View that is about to be inflated
     * 
     * @return True if this class is allowed to be inflated, or false otherwise
     */
    @SuppressWarnings("unchecked")
    boolean onLoadClass(Class clazz);
}

从注释中不难看出Filter是用来限制和过滤View用的,上文提到并非所有的View都能用作远程视图,如果为上述列举的View,则onLoadClass(Class clazz)返回true,否则返回false。

在RemoteViews中实现了Filter接口的方法:

public boolean onLoadClass(Class clazz) {
    return clazz.isAnnotationPresent(RemoteView.class);
}

可以看到就是根据@RemoteView注解来判断是否可以使用该View作为远程视图。

RemoteViews实现原理

跨进程是哪两个进程

很显然我们的应用自身是一个进程,那么另一个进程是什么呢?在RemoteViews的应用场景中,如自定义通知栏和桌面小部件,它们都运行在系统进程中,即SystemServer进程。如此一来,远程视图运行在SystemServer进程中,我们在自己的应用进程中跨进程来操作远程视图。前文说到RemoteViews实现了Parcelable接口,那么RemoteViews便可以从应用进程传输到系统进程了。

远程View是如何加载的

通常情况下,我们使用LayoutInflater加载布局只需要知道布局ID即可。还记得前文讲RemoteViews的构造函数时,有两个重要的字段:

  • mApplication
  • mLayoutId

在系统进程中加载远程视图正是利用了上述两个字段。在RemoteViews的源码中加载布局的逻辑如下:

/**
 * Inflates the view hierarchy represented by this object and applies
 * all of the actions.
 *
 * <p><strong>Caller beware: this may throw</strong>
 *
 * @param context Default context to use
 * @param parent Parent that the resulting view hierarchy will be attached to. This method
 * does <strong>not</strong> attach the hierarchy. The caller should do so when appropriate.
 * @return The inflated view hierarchy
 */
public View apply(Context context, ViewGroup parent) {
    return apply(context, parent, null);
}

接下来会调用:

public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
    RemoteViews rvToApply = getRemoteViewsToApply(context);

    View result = inflateView(context, rvToApply, parent);
    loadTransitionOverride(context, handler);

    rvToApply.performApply(result, parent, handler);

    return result;
}

先重点关注下面这一行代码:

View result = inflateView(context, rvToApply, parent);

其内部实现就是常见的布局加载方式了:

private View inflateView(Context context, RemoteViews rv, ViewGroup parent) {
    // RemoteViews may be built by an application installed in another
    // user. So build a context that loads resources from that user but
    // still returns the current users userId so settings like data / time formats
    // are loaded without requiring cross user persmissions.
    final Context contextForResources = getContextForResources(context);
    Context inflationContext = new ContextWrapper(context) {
        @Override
        public Resources getResources() {
            return contextForResources.getResources();
        }
        @Override
        public Resources.Theme getTheme() {
            return contextForResources.getTheme();
        }
        @Override
        public String getPackageName() {
            return contextForResources.getPackageName();
        }
    };

    LayoutInflater inflater = (LayoutInflater)
            context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

    // Clone inflater so we load resources from correct context and
    // we don't add a filter to the static version returned by getSystemService.
    inflater = inflater.cloneInContext(inflationContext);
    inflater.setFilter(this);
    return inflater.inflate(rv.getLayoutId(), parent, false);
}

主要关注最后几行代码即可,把握主要流程,一些细节可以暂时忽略。

RemoteViews的apply方法中还有这样一行代码:

rvToApply.performApply(result, parent, handler);

接下来会调用:

private void performApply(View v, ViewGroup parent, OnClickHandler handler) {
    if (mActions != null) {
        handler = handler == null ? DEFAULT_ON_CLICK_HANDLER : handler;
        final int count = mActions.size();
        for (int i = 0; i < count; i++) {
            Action a = mActions.get(i);
            a.apply(v, parent, handler);
        }
    }
}

即遍历RemoteViews中存储的Action,然后执行Actionapply方法。

远程View是如何操作的

上一小节提到了Action对象,那么Action又是什么呢?由于RemoteViews运行在远端进程中,因此无法通过findViewById的方法来获取View。为了操作远程视图,于是就将对视图的操作封装成一个Action对象,Action是一个实现了Parcelable接口的抽象类,因此可以跨进程传输。

先来看下Action的定义:

/**
 * Base class for all actions that can be performed on an
 * inflated view.
 *
 *  SUBCLASSES MUST BE IMMUTABLE SO CLONE WORKS!!!!!
 */
private abstract static class Action implements Parcelable {
    public abstract void apply(View root, ViewGroup rootParent,
            OnClickHandler handler) throws ActionException;

    public static final int MERGE_REPLACE = 0;
    public static final int MERGE_APPEND = 1;
    public static final int MERGE_IGNORE = 2;

    public int describeContents() {
        return 0;
    }

    /**
     * Overridden by each class to report on it's own memory usage
     */
    public void updateMemoryUsageEstimate(MemoryUsageCounter counter) {
        // We currently only calculate Bitmap memory usage, so by default,
        // don't do anything here
    }

    public void setBitmapCache(BitmapCache bitmapCache) {
        // Do nothing
    }

    public int mergeBehavior() {
        return MERGE_REPLACE;
    }

    public abstract String getActionName();

    public String getUniqueKey() {
        return (getActionName() + viewId);
    }

    /**
     * This is called on the background thread. It should perform any non-ui computations
     * and return the final action which will run on the UI thread.
     * Override this if some of the tasks can be performed async.
     */
    public Action initActionAsync(ViewTree root, ViewGroup rootParent, OnClickHandler handler) {
        return this;
    }

    int viewId;
}

从说明来看,Action就是对远程视图操作的一个封装,它提供了一个抽象方法:

public abstract void apply(View root, ViewGroup rootParent,
        OnClickHandler handler) throws ActionException;

该抽象方法需要子类做具体实现。

Action有很多子类,几乎每个子类都用来辅助一种View操作,下面简单罗列两个:

/**
 * Helper action to set compound drawables on a TextView. Supports relative
 * (s/t/e/b) or cardinal (l/t/r/b) arrangement.
 */
private class TextViewDrawableAction extends Action {}
/**
 * Helper action to set text size on a TextView in any supported units.
 */
private class TextViewSizeAction extends Action {}

这里不再一一列举,有兴趣的可以参考源码。

了解了Action的概念之后,我们以为远程的TextView设置文本为例,来具体看一下其工作流程。

平时我们给TextView设置文本只需要调用其setText方法即可,但RemoteViews无法这样使用,RemoteViews提供了一系列关于View操作的set方法,这里会用到如下方法:

/**
 * Equivalent to calling TextView.setText
 *
 * @param viewId The id of the view whose text should change
 * @param text The new text for the view
 */
public void setTextViewText(int viewId, CharSequence text) {
    setCharSequence(viewId, "setText", text);
}

其中第一个参数为TextView控件的ID,布局文件是我们自己定义的,因此控件ID可知;第二个参数为要设置的目标文本,接下来继续跟踪源码:

/**
 * Call a method taking one CharSequence on a view in the layout for this RemoteViews.
 *
 * @param viewId The id of the view on which to call the method.
 * @param methodName The name of the method to call.
 * @param value The value to pass to the method.
 */
public void setCharSequence(int viewId, String methodName, CharSequence value) {
    addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
}

可以看到第二个参数为方法名,此时应该可以想到后面会使用反射来为TextView赋值了。接着可以看到果然将对TextView的操作封装成了一个Action,这里的实现子类为ReflectionAction,并将其添加到mActions列表中,需要注意的是此时并未立即执行该View操作,只是添加到了操作列表,至于何时执行,稍后再说,先来看下ReflectionAction的apply实现:

@Override
public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
    final View view = root.findViewById(viewId);
    if (view == null) return;

    Class<?> param = getParameterType();
    if (param == null) {
        throw new ActionException("bad type: " + this.type);
    }

    try {
        getMethod(view, this.methodName, param).invoke(view, wrapArg(this.value));
    } catch (ActionException e) {
        throw e;
    } catch (Exception ex) {
        throw new ActionException(ex);
    }
}

正如我们猜测,内部就是利用反射机制为TextView来设置文本的。

上文说到只是暂时将Action添加到列表了,对于何时执行这些操作,要结合应用场景来说。对于自定义通知栏,需要NotificationManager调用notify()之后;而对于桌面小部件,则需要AppWidgetManager调用updateAppWidget()之后。至于为什么不实时调用,大概是基于频繁跨进程视图操作的性能开销及效率考量吧。

对于View操作的执行对应着每个Action的apply方法的调用,在RemoteViews中则对应着apply与reapply两种方式。它们的区别是:apply加载视图并执行View操作(前文已分析过),用于通知栏和桌面小部件的初始化操作;而reapply则适用于远程视图的更新操作。

PendingIntent简介

除了更新远程视图的UI,往往还需要为远程视图添加点击事件,可想而知同样不能像以往那样设置setOnClickListener,这里就需要用到PendingIntent。

顾名思义,PendingIntent可以理解为等待的或即将发生的意图,但也不要被其名字误导,它并非继承Intent,其定义如下:

public final class PendingIntent implements Parcelable {}

或者可以把PendingIntent理解为一种特殊的异步处理机制,且它往往是跨进程执行,因此实现了Parcelable接口。

PendingIntent提供了3种类型:

  • 启动Activity
  • 启动Service
  • 发送广播

对应于如下方法:

public static PendingIntent getActivity(Context context, int requestCode,
            Intent intent, @Flags int flags)
public static PendingIntent getActivity(Context context, int requestCode,
            @NonNull Intent intent, @Flags int flags, @Nullable Bundle options)
public static PendingIntent getService(Context context, int requestCode,
            @NonNull Intent intent, @Flags int flags) 
public static PendingIntent getBroadcast(Context context, int requestCode,
        Intent intent, @Flags int flags)

这里先简单了解PendingIntent即可,关于PendingIntent的使用下文会涉及。如果想深入理解PendingIntent,推荐一篇文章:说说PendingIntent的内部机制

RemoteViews的应用

自定义通知栏

先来看展示自定义通知栏的核心代码:

public void showNotification() {
    mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    mNotification = new Notification();
    mNotification.icon = R.drawable.ic_star;
    mNotification.tickerText = "Serendipity";
    mNotification.when = System.currentTimeMillis();

    mContentView = new RemoteViews(getPackageName(), R.layout.notification_layout);
    // 可覆盖xml中定义的text
    // mContentView.setTextViewText(R.id.text11, "other text");

    // 设置常驻,不能滑动取消
    // mNotification.flags = Notification.FLAG_ONGOING_EVENT;
    // 默认跳转主界面
    mIntent = new Intent(this, MainActivity.class);
    mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    mPendingIntent = PendingIntent.getActivities(this, 0, new Intent[]{mIntent}, PendingIntent.FLAG_UPDATE_CURRENT);

    // 每个view的点击响应
    mContentView.setOnClickPendingIntent(R.id.ll_11, PendingIntent.getBroadcast(NotificationActivity.this, 0, new Intent().setAction(ACTION_ONE), PendingIntent.FLAG_UPDATE_CURRENT));
    mContentView.setOnClickPendingIntent(R.id.ll_22, PendingIntent.getBroadcast(NotificationActivity.this, 0, new Intent().setAction(ACTION_TWO), PendingIntent.FLAG_UPDATE_CURRENT));
    mContentView.setOnClickPendingIntent(R.id.ll_33, PendingIntent.getBroadcast(NotificationActivity.this, 0, new Intent().setAction(ACTION_THREE), PendingIntent.FLAG_UPDATE_CURRENT));
    mContentView.setOnClickPendingIntent(R.id.ll_44, PendingIntent.getBroadcast(NotificationActivity.this, 0, new Intent().setAction(ACTION_FOUR), PendingIntent.FLAG_UPDATE_CURRENT));

    mNotification.contentView = mContentView;
    mNotification.contentIntent = mPendingIntent;
    mNotificationManager.notify(notificationId, mNotification);
}

其中代码

mContentView = new RemoteViews(getPackageName(), R.layout.notification_layout);

从一个布局文件中加载视图,该布局文件的定义如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:gravity="center">

    <LinearLayout
        android:id="@+id/ll_11"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:orientation="vertical">

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:src="@drawable/ic_menu_gallery"
            android:textColor="#000" />

        <TextView
            android:id="@+id/text11"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="Action1"
            android:textColor="#000" />
    </LinearLayout>

    <LinearLayout
        android:id="@+id/ll_22"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:orientation="vertical">

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:src="@drawable/ic_menu_manage"
            android:textColor="#000" />

        <TextView
            android:id="@+id/text22"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="Action2"
            android:textColor="#000" />
    </LinearLayout>

    <LinearLayout
        android:id="@+id/ll_33"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:orientation="vertical">

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:src="@drawable/ic_menu_send"
            android:textColor="#000" />

        <TextView
            android:id="@+id/text33"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="action3"
            android:textColor="#000" />
    </LinearLayout>

    <LinearLayout
        android:id="@+id/ll_44"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:orientation="vertical">

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:src="@drawable/ic_menu_share"
            android:textColor="#000" />

        <TextView
            android:id="@+id/text44"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="action4"
            android:textColor="#000" />
    </LinearLayout>

</LinearLayout>

布局很简单,以上图下文的格式并排放了四个按钮。

接下来的代码为四个按钮设置了点击事件,此处使用的是广播类型的PendingIntent,并分别为Intent设置了不同的Action,后续通过自定义的BroadcastReceiver来接收广播,并根据接收到的Intent的Action进行响应。

对于整个通知栏则使用了Activity类型的PendingIntent,因此点击时会跳转主界面。

最终的效果图如下:

这里写图片描述

这里写图片描述

桌面小部件

桌面小部件在新版本的Android系统上入口比较隐蔽了,在Android 7.1.1上(Nexus 6P)需要在主屏空白处长按,将会出现小部件的入口。

这里我们制作一个简单的小部件来作为示例:该小部件也是上图下文,文本来显示当前时间,并且每秒更新。最终效果如下:

这里写图片描述

首先定义小部件的视图布局,在res/layout文件夹中创建app_widget.xml,其内容如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/twitter_png" />

    <TextView
        android:id="@+id/tv_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello Twitter"/>

</LinearLayout>

布局比较简单,上面为一个ImageView,下面是一个TextView,稍后我们会使用当前时间来远程更新TextView的内容。

接下来定义小部件的配置文件,在res/xml目录中创建app_widget_config.xml文件,其内容如下:

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/app_widget"
    android:minHeight="200dp"
    android:minWidth="150dp"
    android:updatePeriodMillis="50000" />

其中initialLayout指向小部件的视图布局,updatePeriodMillis规定了小部件的更新周期,系统为了节电,默认30分钟更新一次,即便设置的时间小于30分钟,也是30分钟更新一次。

然后我们再定义一个服务,用于更新小部件中的TextView,该服务的代码如下:

public class TestService extends Service {
    private Timer mTimer;
    private SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mTimer = new Timer();
        mTimer.schedule(new TimerTask() {
            @Override
            public void run() {
                updateWidget();
            }
        }, 0, 1000);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mTimer != null) {
            mTimer.cancel();
            mTimer = null;
        }
    }

    public void updateWidget() {
        String time = f.format(new Date());
        RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.app_widget);
        remoteViews.setTextViewText(R.id.tv_content, time);

        AppWidgetManager manager = AppWidgetManager.getInstance(getApplicationContext());
        ComponentName cn = new ComponentName(getApplicationContext(), WidgetProvider.class);
        manager.updateAppWidget(cn, remoteViews);
    }
}

在服务中运行一个定时器,每隔一秒调用AppWidgetManager的updateAppWidget来更新小部件。用来更新TextView的代码如下:

RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.app_widget);
remoteViews.setTextViewText(R.id.tv_content, time);

其原理在前文中已有分析。

桌面小部件还要依赖于AppWidgetProvider,AppWidgetProvider本质上是一个广播接收器,从其定义可以验证:

public class AppWidgetProvider extends BroadcastReceiver {}

我们自定义了一个AppWidgetProvider,代码如下:

package com.aspook.appwidget;

import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;

public class WidgetProvider extends AppWidgetProvider {
    @Override
    public void onDisabled(Context context) {
        super.onDisabled(context);
        context.stopService(new Intent(context, TestService.class));
    }

    @Override
    public void onDeleted(Context context, int[] appWidgetIds) {
        super.onDeleted(context, appWidgetIds);
    }

    @Override
    public void onEnabled(Context context) {
        super.onEnabled(context);
        context.startService(new Intent(context, TestService.class));
    }

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        super.onReceive(context, intent);
    }

    @Override
    public void onRestored(Context context, int[] oldWidgetIds, int[] newWidgetIds) {
        super.onRestored(context, oldWidgetIds, newWidgetIds);
    }

    public WidgetProvider() {
        super();
    }

    @Override
    public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) {
        super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
    }
}

上述代码onEnabled中会启动服务,onDisabled中停止服务,同时还有其他涉及小部件生命周期的回调。

最后需要在AndroidManifest.xml中注册广播接收器与服务:

<service android:name="com.aspook.appwidget.TestService" />
<receiver android:name="com.aspook.appwidget.WidgetProvider">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>
    <meta-data
        android:name="android.appwidget.provider"
        android:resource="@xml/app_widget_config" />
</receiver>

上述步骤完毕之后,当你将上述代码运行到手机上,就可以从小部件库中找到刚才定义的小部件了,你可以把它拖到桌面上,会发现小部件的TextView每隔一秒都会更新当前时间,如最初的效果图所示。

以上是关于RemoteViews原理分析及应用的主要内容,如果未能解决你的问题,请参考以下文章

Android:将位图从本地存储加载到应用小部件(RemoteViews)

Android Remote Views

带有 RemoteViews 的 Android 自定义通知布局

是否可以在其他应用程序中充气 RemoteViews?

Mybatis-Plus的应用场景及注入SQL原理分析

JVM CPU Profiler技术原理及源码深度解析