Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> RemoteViews完全解析

RemoteViews完全解析

編輯:關於Android編程

一、概述

什麼是RemoteViews?從字面意思可以理解為遠程View。需要注意的是,RemoteViews並不是View的子類,它是Object的子類。它的作用是可以跨進程更新界面,是不是功能很強大?它在實際應用中主要在通知欄和桌面小控件,下面就全部基於通知欄來分析。

二、演示

這裡寫圖片描述

這裡寫圖片描述

三、RemoteViews在通知欄上的應用

定義了兩個按鈕,分別用來發送不同的通知


    

java代碼


    case R.id.btn_normal:
            //1.創建通知對象
            Notification nf = new Notification() ;
            //2.通知的一些常用設置
            //圖標
            nf.icon = R.drawable.ic_launcher ;
            //通知時間
            nf.when = System.currentTimeMillis();
            //消息來臨時顯示的提示信息
            nf.tickerText = "這是普通的通知";
            //設置通知自動取消
            nf.flags = Notification.FLAG_AUTO_CANCEL;
            //3.創建延時意圖
            Intent intent = new Intent(this,NormalNotificationActivity.class);
            PendingIntent pi = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
            //
            nf.setLatestEventInfo(this, "普通通知", "這是來自普通通知的摘要信息", pi);
            //4.創建notificationmanager對象
            NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
            //這裡的id最好保證每次都是不一樣的,否則第二次發送無效
            nm.notify(nId++ , nf);
            break;

這只是一個簡單的通知,並沒有涉及RemoteViews,主要是知道普通的通知應該如何書寫。


    case R.id.btn_zdy:
            //1.創建通知對象
            Notification myNf = new Notification() ;
            //2.通知的一些常用設置
            //圖標
            myNf.icon = R.drawable.ic_launcher ;
            //通知時間
            myNf.when = System.currentTimeMillis();
            myNf.tickerText = "這是自定義的通知";
            myNf.flags = Notification.FLAG_AUTO_CANCEL;
            //3.創建延時意圖
            Intent myIntent = new Intent(this,NormalNotificationActivity.class);
            PendingIntent myPi = PendingIntent.getActivity(this, 0, myIntent, PendingIntent.FLAG_UPDATE_CURRENT);
            /**
             * 注意:這個字段是要配合nf.contentView這個字段一起使用的,如果只是設置一個會報錯
             * nf.contentIntent = pi ;
             */
            /******************
             *沒有設置contentIntent時:java.lang.IllegalArgumentException: contentIntent required: pkg=com.lw.remoteviewsdemo id=0 notification=Notification(vibrate=null,sound=null,defaults=0x0,flags=0x10)
             * ****************************/
            //4.創建RemoteViews
            RemoteViews rv = new RemoteViews(getPackageName(),R.layout.activity_mynotification);

            //給remoteviews手動設置值
            rv.setTextViewText(R.id.msg, "我是自定義通知");
            rv.setTextViewText(R.id.des, "我是對自定義通知的一些簡單描述信息");

            myNf.contentView = rv ;
            myNf.contentIntent = myPi ;
            //5.創建notificationmanager對象
            NotificationManager myNm= (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
            //這裡的id最好保證每次都是不一樣的,否則第二次發送無效
            myNm.notify(nId++ , myNf);
            break;

這是自定義通知,上面的都有注釋,就幾個注意的地方說下,使用自定義通知需要為通知的這兩個屬性賦值,否則就會報上面提到的異常,少一個都不行。


    myNf.contentView = rv ;
    myNf.contentIntent = myPi ;

還有一點就是在通知myNm.notify(nId++ , myNf)的第一個參數不要設置為一個常量,否則只能發送一個通知。

四、RemoteViews源碼分析

在概述中提到,RemoteViews是跨進程更新UI的,也就是通過Binder,那麼也就是說RemoteViews肯定實現了序列化接口,看源碼:


    public class RemoteViews implements Parcelable, Filter 

再看上面的例子,RemoteViews是如何自定義一些文本內容的呢,大部分都是通過setTextViewText()等方法完成的,看源碼


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

調用了setCharSequence方法,注意:這裡的第二個參數是setText,是不是和TextView的setText很像呢,點進去


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

那麼ReflectionAction又是什麼東西呢?從字面可以理解為反射動作。


    /**
     * Base class for the reflection actions.
     */
    private class ReflectionAction extends Action {
        static final int TAG = 2;

        static final int BOOLEAN = 1;
        static final int BYTE = 2;
        static final int SHORT = 3;
        static final int INT = 4;
        static final int LONG = 5;
        static final int FLOAT = 6;
        static final int DOUBLE = 7;
        static final int CHAR = 8;
        static final int STRING = 9;
        static final int CHAR_SEQUENCE = 10;
        static final int URI = 11;
        // BITMAP actions are never stored in the list of actions. They are only used locally
        // to implement BitmapReflectionAction, which eliminates duplicates using BitmapCache.
        static final int BITMAP = 12;
        static final int BUNDLE = 13;
        static final int INTENT = 14;

從源碼可以看出,它是Action的子類,從注釋可以看出它是反射類的基類,通過之前傳遞過來的setText來進行反射獲取信息,這個類需要配合一會分析的RemoteViews的apply()方法配合看。再回到前面的addAction方法


     /**
     * Add an action to be executed on the remote side when apply is called.
     *
     * @param a The action to add
     */
    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();
        }
        mActions.add(a);

        // update the memory usage stats
        a.updateMemoryUsageEstimate(mMemoryUsageCounter);
    }

可以看出,系統首先判斷mActions是否為null,如果為null,則創建一個存放Action的List集合,然後就將傳遞過來的Action添加到mActions中。再來看一些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
            return;
        }

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

        public int mergeBehavior() {
            return MERGE_REPLACE;
        }

        public abstract String getActionName();

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

        int viewId;
    }

從注釋可以看出它是一個在被填充的展現出來的所有Action的基類,實現Parcelable接口。
簡單來說setTextViewText(int viewId, CharSequence text) 方法首先將其封裝成一個動作Action類,然後添加到一個actions集合中去。

接著,再來看看RemoteViews的apply方法


    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;

        Context c = prepareContext(context);

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

        inflater = inflater.cloneInContext(c);
        inflater.setFilter(this);

        result = inflater.inflate(rvToApply.getLayoutId(), parent, false);

        rvToApply.performApply(result, parent, handler);

        return result;
    }

首先,通過上下文獲取RemoteViews,然後調用performApply()方法,再看下performApply()源碼


    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);
            }
        }
    }

這裡就可以看出來了,首先判斷mActions集合是否為null,不為null,依次遍歷actions,然後將處理交給action的apply方法,由於之前我調用的是setTextViewText方法,在那裡添加的action的是ReflectionAction,所以這裡查看的是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);
            }

            Class klass = view.getClass();
            Method method;
            try {
                method = klass.getMethod(this.methodName, getParameterType());
            }
            catch (NoSuchMethodException ex) {
                throw new ActionException("view: " + klass.getName() + " doesn't have method: "
                        + this.methodName + "(" + param.getName() + ")");
            }

            if (!method.isAnnotationPresent(RemotableViewMethod.class)) {
                throw new ActionException("view: " + klass.getName()
                        + " can't use method with RemoteViews: "
                        + this.methodName + "(" + param.getName() + ")");
            }

            try {
                //noinspection ConstantIfStatement
                if (false) {
                    Log.d(LOG_TAG, "view: " + klass.getName() + " calling method: "
                        + this.methodName + "(" + param.getName() + ") with "
                        + (this.value == null ? "null" : this.value.getClass().getName()));
                }
                method.invoke(view, this.value);
            }
            catch (Exception ex) {
                throw new ActionException(ex);
            }
        }

從上面的代碼可以看出,主要是通過反射來執行代碼,而方法名之前也已經有了,只需要調用invoke即可。OK,這就是RemoteViews的大致流程了。下面就這種思想使用RemoteViews來實現一個跨進程更新UI的例子

五、使用RemoteViews跨進程更新UI的Demo

1.思路

創建兩個應用,一個作為本地,一個作為遠程,當然如果想要在一個應用使用兩個Activity實現也是可以的,只需要在其中一個你想要的Activity作為遠程Activity中的配置文件中指定以下代碼即可,我這裡就是使用兩個應用,效果觀看更加明顯。

    android:process=":remote"

通知欄的跨進程是通過Binder實現的,我這裡就通過廣播接受者來實現。OK,上代碼

2.代碼

本地布局文件


    

    
    

     

子LinearLayout是用來存放從遠程傳遞過來的布局的。

本地java代碼


    public class MainActivity extends Activity {
    private IntentFilter filter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ll_layout = (LinearLayout) findViewById(R.id.ll_layout);
        filter = new IntentFilter("com.lw.remoteviewstest1");
        registerReceiver(mRemoteViewsReceiver, filter);

    }

    private BroadcastReceiver mRemoteViewsReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            RemoteViews remoteViews = intent
                    .getParcelableExtra("remoteViews");
            if (remoteViews != null) {
                updateLayout(remoteViews);
            }
        }
    };
    private LinearLayout ll_layout;

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //反注冊廣播接受者
        if(mRemoteViewsReceiver != null ){
            unregisterReceiver(mRemoteViewsReceiver);
        }
    }
    /**
     * 根據remoteViews更新ui
     * @param remoteViews
     */
    protected void updateLayout(RemoteViews remoteViews) {
        View view = remoteViews.apply(this, ll_layout);
        ll_layout.addView(view);
    };

}

主要在onCreate動態創建一個廣播接收者,然後再onDestory中反注冊,當從遠程獲取到remoteViews的時候調用updateLayout的方法,通過remoteViews的apply方法,這個方法前面已經詳細分析了,就不在談了。

遠程布局文件


    

    

遠程java代碼


     public void send(View view){
        //創建remoteviews
        RemoteViews rv = new RemoteViews(getPackageName(),R.layout.activity_notification);
        //創建intent,設置當點擊按鈕時要跳轉的Activity
        Intent sendIntent = new Intent(this,SecondActivity.class);
        //創建延時意圖
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, sendIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        //為延時意圖中的按鈕設置點擊事件
        rv.setOnClickPendingIntent(R.id.btn_toast, pendingIntent) ;
        //創建隱式跳轉
        Intent intent = new Intent("com.lw.remoteviewstest1");
        //將RemoteViews帶到應用一中去
        intent.putExtra("remoteViews", rv);
        //發送廣播
        sendBroadcast(intent);
    }

這裡提下,如果想要為remoteviews添加點擊事件,則必須使用PendingIntent,其實最後還是通過添加action,不過這裡的action不是前面提到的反射action,而是SetOnClickPendingIntent,我們可以看下這個類的apply方法,這裡只需要看最後一個代碼,最後調用了setOnclickListener方法,其它的注釋都比較詳細,就不說了。


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

            // If the view is an AdapterView, setting a PendingIntent on click doesn't make much
            // sense, do they mean to set a PendingIntent template for the AdapterView's children?
            if (mIsWidgetCollectionChild) {
                Log.w(LOG_TAG, "Cannot setOnClickPendingIntent for collection item " +
                        "(id: " + viewId + ")");
                ApplicationInfo appInfo = root.getContext().getApplicationInfo();

                // We let this slide for HC and ICS so as to not break compatibility. It should have
                // been disabled from the outset, but was left open by accident.
                if (appInfo != null &&
                        appInfo.targetSdkVersion >= Build.VERSION_CODES.JELLY_BEAN) {
                    return;
                }
            }

            if (target != null) {
                // If the pendingIntent is null, we clear the onClickListener
                OnClickListener listener = null;
                if (pendingIntent != null) {
                    listener = new OnClickListener() {
                        public void onClick(View v) {
                            // Find target view location in screen coordinates and
                            // fill into PendingIntent before sending.
                            final float appScale = v.getContext().getResources()
                                    .getCompatibilityInfo().applicationScale;
                            final int[] pos = new int[2];
                            v.getLocationOnScreen(pos);

                            final Rect rect = new Rect();
                            rect.left = (int) (pos[0] * appScale + 0.5f);
                            rect.top = (int) (pos[1] * appScale + 0.5f);
                            rect.right = (int) ((pos[0] + v.getWidth()) * appScale + 0.5f);
                            rect.bottom = (int) ((pos[1] + v.getHeight()) * appScale + 0.5f);

                            final Intent intent = new Intent();
                            intent.setSourceBounds(rect);
                            handler.onClickHandler(v, pendingIntent, intent);
                        }
                    };
                }
                target.setOnClickListener(listener);
            }
        }

Demo整體思路:遠程主要通過Intent攜帶RemoteViews傳遞到本地連接,本地連接獲取到RemoteViews之後,調用apply方法返回個View對象,這個view對象就是遠程RotateViews攜帶的布局,然後添加到本地對應的布局中去。

OK,這篇關於RemoteViews的學習總結就到這了

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved