zoukankan      html  css  js  c++  java
  • View

    设计Android的工程师起名字还是挺规范的,而且一眼就知道是什么意思。RemoteViews,顾名思义,远程的View。Android为了能让进程A显示进程B的View,设计了这么一种View(其实不是真正的View)。其实我们开发过程中,发通知到状态栏显示也是利用了RemoteViews,我们来了解一下RemoteViews吧。

    我们先看看RemoteViews怎么配合Notification使用:

    import android.annotation.SuppressLint;
    import android.app.Activity;
    import android.app.Notification;
    import android.app.NotificationManager;
    import android.app.PendingIntent;
    import android.content.Context;
    import android.content.Intent;
    import android.os.Bundle;
    import android.widget.RemoteViews;
    
    @SuppressLint("NewApi")
    public class MainActivity extends Activity {
    
        private RemoteViews contentView;
        private Notification notification;
        private NotificationManager notificationManager;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            sendNotification();
        }
    
        private void sendNotification() {
            contentView = new RemoteViews(getPackageName(), R.layout.layout_remote);
            contentView.setTextViewText(R.id.remote_title, "Remote View Title");
            contentView.setTextViewText(R.id.remote_content, "This Remote View Content ... 
    This Remote View Content ... 
    This Remote View Content ...");
            Intent intent = new Intent(this, MainActivity.class);
            PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
            // RemoteViews的事件只能是PendingIntent
            contentView.setOnClickPendingIntent(R.id.remote_content, pendingIntent);
            notification = new Notification.Builder(this) 
                    .setWhen(System.currentTimeMillis())    // 设置显示通知的时间
                    .setAutoCancel(true)                    // 设置是否可以手动取消
                    .setSmallIcon(R.mipmap.ic_launcher)     // 设置在状态栏的小图标,如果没有设置,不显示通知
                    .setCustomBigContentView(contentView)   // 设置自定义View,setCustomBigContentView可以显示remoteviews的完整高度,setCustomContentView只能显示系统通知栏高度。
                    .build();
            notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
            // 发通知
            notificationManager.notify(1, notification);
        }
    
    }

    其中R.layout.layout_remote布局文件如下:

    <?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="wrap_content"
        android:padding="10dp"
        android:orientation="vertical" >
    
        <TextView 
            android:id="@+id/remote_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="18sp"
            android:textColor="@android:color/holo_blue_light"
            />
    
        <TextView 
            android:id="@+id/remote_content"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="3dp"
            android:textSize="16sp"
            android:textColor="@android:color/darker_gray"
            />
    
    </LinearLayout>

    效果如图所示:

    因为我是调用setCustomBigContentView来加载RemoteViews的,所以RemoteViews可以显示完整,不受系统通知栏高度限制。

    我们接下来解析状态栏是怎么加载我们定义的RemoteViews的,Let’s Go !!

     RemoteViews加载

    我们首先要知道状态栏是SystemServer进程,而我们定义的RemoteViews是在我们App进程,状态栏要加载并显示我们的RemoteViews,这肯定是通过IPC,主要实现是Binder。

    RemoteViews会通过Binder传递给SystemServer进程,系统会根据RemoteViews的包名和布局id等信息,获取到应用的资源(布局文件,图标等),然后通过LayoutInflater加载RemoteViews中的布局文件,最后在状态栏和通知栏显示出来。

    RemoteViews更新

    RemoteViews提供了很多个方法,更新RemoteViews的布局文件:

    // 部分方法
    - setTextViewText(viewId, text)                     设置文本
    - setTextColor(viewId, color)                       设置文本颜色
    - setTextViewTextSize(viewId, units, size)          设置文本大小 
    - setImageViewBitmap(viewId, bitmap)                设置图片
    - setImageViewResource(viewId, srcId)               根据图片资源设置图片
    - setViewPadding(viewId, left, top, right, bottom)  设置Padding间距
    - setOnClickPendingIntent(viewId, pendingIntent)    设置点击事件 
    - setInt(viewId, methodName, value)                 反射调用参数为int的methodName方法
    - setLong(viewId, methodName, value)                反射调用参数为long的methodName方法
    ...

    当调用以上方法来更新RemoteViews时,RemoteViews并 不会立刻更新,只是封装了一系列的Action,然后等待时机更新。

    我们从源码分析,当我们调用setTextViewText来更新内容时:

    private void updateNotification() {
        contentView.setTextViewText(R.id.remote_content, "This Remote View Update Content ... 
    This Remote View Update Content ... 
    This Remote View Update Content ...");
        notificationManager.notify(1, notification);
    }

    我们来看看setTextViewText源码:

    // RemoteViews类
    public void setTextViewText(int viewId, CharSequence text) {
        setCharSequence(viewId, "setText", text);
    }

    调用了setCharSequence方法:

    // RemoteViews类
    public void setCharSequence(int viewId, String methodName, CharSequence value) {
        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
    }

    addAction方法:

    // RemoteViews类
    private void addAction(Action a) {
        if (hasLandscapeAndPortraitLayouts()) {
                    throw new RuntimeException("RemoteViews specifying separate landscape and portrait" +
                            " layouts cannot be modified. Instead, fully configure the landscape and" +
                            " portrait layouts individually before constructing the combined layout.");
                }
        if (mActions == null) {
            mActions = new ArrayList<Action>();
        }
        mActions.add(a);
        a.updateMemoryUsageEstimate(mMemoryUsageCounter);
    }

    可以看出,整个set过程,只是封装了一个Action并添加到mActions(一个List)中,所以这个过程并没有更新RemoteViews哦。我们看看ReflectionAction是什么:

    // ReflectionAction类
    private final class ReflectionAction extends Action {
        ...
        ReflectionAction(int viewId, String methodName, int type, Object value) {
             this.viewId = viewId;
             this.methodName = methodName;
             this.type = type;
             this.value = value;
        }
        ...
    }     

    就是储存了一些属性,主要是传递给SystemServer进程的一些更新RemoteViews布局信息。

    当我们调用notificationManager.notify(1, notification)方法,RemoteViews布局才会开始更新。 
    我们来看看notify代码:

    // NotificationManager类
    public void notify(int id, Notification notification){
        notify(null, id, notification);
    }
    public void notify(String tag, int id, Notification notification){
        notifyAsUser(tag, id, notification, new UserHandle(UserHandle.myUserId()));
    }

    最终调用了notifyAsUser方法:

    // NotificationManager类
    public void notifyAsUser(String tag, int id, Notification notification, UserHandle user){
        ...
        INotificationManager service = getService();
        ...
        final Notification copy = Builder.maybeCloneStrippedForDelivery(notification);
        try {
            service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
                    copy, idOut, user.getIdentifier());
            ...
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    service为INotificationManager的代理对象,调用了enqueueNotificationWithTag方法后,通过Binder,也就调用了NotificationManagerService(INotificationManager的存根对象,存在于SystemServer进程)的enqueueNotificationWithTag方法:

    // NotificationManagerService类
    public void enqueueNotificationWithTag(String pkg, String packageName, String tag, int id, Notification notification, int[] idOut, @UserIdInt userIdInt) {
        ...
        StatusBarNotification n = new StatusBarNotification(pkg, id, tag, r.uid, r.initialPid, notification);
        try {                      
            mStatusBar.updateNotification(r.statusBarKey, n) 
            ...
        }
        ...
    }

    调用了StatusBarNotification的updateNotification方法:

    // StatusBarNotification类
    public void updateNotification(IBinder key, StatusBarNotification notification) {
        ...
         final RemoteViews contentView = notification.notification.contentView;
        ...
        contentView.reapply(mContext, oldEntry.content);
        ...
    }

    最终在SystemServer进程调用了RemoteViews的reapply方法:

    // RemoteViews类
    public void reapply(Context context, View v) {
        reapply(context, v, null);
    }
    public void reapply(Context context, View v, OnClickHandler handler) {
        RemoteViews rvToApply = getRemoteViewsToApply(context);
        if (hasLandscapeAndPortraitLayouts()) {
            if (v.getId() != rvToApply.getLayoutId()) {
                throw new RuntimeException("Attempting to re-apply RemoteViews to a view that" +
                        " that does not share the same root layout id.");
            }
        }
        rvToApply.performApply(v, (ViewGroup) v.getParent(), handler);
    }

    最终调用了RemoteViews的performApply方法:

    // RemoteViews类
    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);
            }
        }
    }

    我们之前setXXX方法时不是储存了Action吗,通过调用performApply方法,遍历所有Action,然后更新RemoteViews的布局文件。代码中,调用了Action的apply方法实现View的更新,Action是一个抽象类,apply方法由子类实现。我们看看ReflectionAction类的apply方法:

    // ReflectionAction类
    private final class ReflectionAction extends Action {
        ...
        @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) {
                throws new ActionException("bad type : " + this.type);
            }
            try {
                getMethod(view, this.methodName, param).invoke(view, wrapArg(this.value));
            } catch (ActionException e) {
                throws e;
            } catch (Exception ex) {
                throws new ActionException(ex);
            }
        }
        ...
    }     

    主要是通过反射,调用View的方法,更新View。

     注意

    RemoteViews设置的布局文件并不支持所有的View,以下是RemoteViews所支持的View:

    layout

    FrameLayout,LinearLayout,RelativeLayout,GridLayout

    view

    Button,ImageView,ImageButton,TextView,ProgressBar,ListView,GridView,StackView,ViewStub,AdapterViewFlipper,ViewFlipper,AnalogClock,Chronometer

     小结

    通过对RemoteViews的了解,我们灵活的设计出多样式的RemoteViews,还可以在不用应用(A)显示自己应用(B)想要显示的View,这个有需要再探索。

    转: https://blog.csdn.net/johanman/article/details/76019771

  • 相关阅读:
    Google和百度、雅虎的站内搜索代码
    Unidac:解决“trying to modify readonly Field”问题!
    如何通过BDE连接一个非1433端口的SQL SERVER
    工作总结
    iOS开发肯定会遇到的
    自定义delegate和系统定义dalegate的区别
    C语言中关于字符串左右循环移位的问题
    iphone 文件保存策略
    iphone学习各种资源
    IOS开发系列之阿堂教程:tableView的下拉涮新功能实践
  • 原文地址:https://www.cnblogs.com/blosaa/p/9564332.html
Copyright © 2011-2022 走看看