《Android開發藝術探索》之理解RemoteViews、BroadCastReceiver(六)

                                                                           第五章 理解RemoteViews
       RemoteViews可以理解爲一種遠程的View,其實他和遠程的Service是一樣的。一個View結構,可以在其他進程中顯示,可以提供一組基礎的操作用於跨進程更新它的界面。應用場景是通知欄和桌面小部件,本章內容包括:RemoteViews在通知欄和桌面小部件的應用、RemoteViews的內部機制、分析RemoteViews的意義並給出一個採用RemoteViews跨進程更新界面的示例。越來越覺得存在着理解能力不足的問題,這本書太偏理論了。
(一)RemoteViews的應用
        主要是是通知欄和桌面小部件,前者通過NotificationManager的notify方法去實現通知欄,可定義佈局。桌面小部件由AppwidgetProvider(本質廣播)來實現,但缺陷是:他們的更新都無法像Activity中直接更新View,這是因爲兩者都運行在其他進程SystemService中,爲了跨進程更新UI,RemoteViews提供了一系列的set方法,我們接下來就是來實際的演示了。
        NotificationManager的代碼:

//複習通知。主要是通過NotificationManager 和pendingIntent來實現的。
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private Button btn_send_notice;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btn_send_notice = (Button) findViewById(R.id.btn_send_notice);
        btn_send_notice.setOnClickListener(this);
    }
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_send_notice:
                //步驟四:點擊通知沒有效果,使用pendingIntent來實現(延遲執行的Intent)
                //可以getActivity、getService、getBroadcast,第一個參數context,第二個沒用,第三個intent對象,第四個是PI的行爲,一般爲零。
                //最後記得setContentIntent(pi)
                Intent intent = new Intent(MainActivity.this,Main2Activity.class);
                PendingIntent pi = PendingIntent.getActivity(this,0,intent,0);
                //步驟一:NotificationManager對通知進行管理,調用Context的getSystemService可獲得,該API可用於確定獲取系統的哪一個服務
                NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
                //步驟二:通過Builder構造器來創建Notification對象,可以連綴任意多的設置方法來創建對象,注意是support-v4庫的內容,兼容性好
                //設置內容包括標題、正文、創建時間、通知小圖標、大圖標。
                Notification notification = new NotificationCompat.Builder(this)
                        .setContentTitle("This is content title")
                        .setContentText("This is content text")
                        .setWhen(System.currentTimeMillis())
                        .setSmallIcon(R.mipmap.ic_launcher)
                        .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
                        .setContentIntent(pi)
                        .setAutoCancel(true)
                        .build();
                //步驟三:調用NotificationManager的notify就可以讓通知顯示出來了,notify接收兩個參數,一個是id,確保每個通知的id是不同的,
                //另一個是Notification對象
                notificationManager.notify(1,notification);
                //步驟五:點擊之後取消通知,在新的Activity中寫這個,cancel的1指的是剛纔notify接收兩個參數中,或者setAutoCancel
//                NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
//                manager.cancel(1);
                break;
            default:
                break;
        }
    }
}

1.1.RemoteViews在通知欄上的應用
       傳統默認樣式如上所示,比較簡單,爲了滿足個性化需求,自定義使用RemoteViews來加載自定義的佈局文件即可改變通知樣式。NotificationManager+PendingIntent+RemoteViews

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_send_notice:
                Intent intent = new Intent(MainActivity.this, Main2Activity.class);
                PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);

                NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

                RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_notification);
                remoteViews.setTextViewText(R.id.msg, "chapter_5");
                remoteViews.setImageViewResource(R.id.icon, R.mipmap.ic_launcher);
                remoteViews.setOnClickPendingIntent(R.id.open_activity2, pi);

                Notification notification = new NotificationCompat.Builder(this)
                        .setContentTitle("This is content title")
                        .setContentText("This is content text")
                        .setWhen(System.currentTimeMillis())
                        .setSmallIcon(R.mipmap.ic_launcher)
                        .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
                        .setContentIntent(pi)
                        .setCustomContentView(remoteViews)
                        .setAutoCancel(true)
                        .build();

                notificationManager.notify(2, notification);
                break;
            default:
                break;
        }
}

       RemoteViews的使用也很簡單,只要提供當前應用的包名和佈局文件的資源id就可以創建一個RemoteViews了,如何更新呢?這一點和View還是有很大的不同,RemoteViews更新的時候,無法直接訪問裏面的View,必須通過RemoteViews所提供的一系列方法來更新,比如設置TextView,那就需要remoteViews.setTextViewText(R.id.msg, "chapter_5");
       如果需要點擊事件的話,需要setOnClickPendingIntent來觸發了,關於PendingIntent,他表示點擊就是一種待定的意圖,觸發後纔會操作。
1.2.RemoteViews 在桌面小部件的應用
       AppWidgetProvider是Android提供給我們的用於實現桌面小部件的類,其本質也就是一個廣播BroadCastReceiver,繼承自Object和BroadCastReceiver。因此將它當做BroadCastReceiver即可。
        步驟一:定義小部件視圖:在res/layout下我們先寫個widget.xml,這裏就是小部件的視圖了。
        步驟二:定義小部件配置信息:在res/xml中定義一個appwidget_info.xml,initialLayout是小工具所使用的初始化佈局,minHeight和minWidth是最小尺寸,updatePeriodMillis是自動更新週期,每隔一個週期,小工具自動更新會觸發。
        步驟三:實現小部件的實現類,繼承自AppWidgetProvider,初始化界面和後續的更新界面都必須使用RemoteViews來完成。
        步驟四:在XML文件中聲明小部件:本質是小部件,必須要註冊。有兩個Action,其中第一個是識別小部件的動作,第二個就是他的標識。
1.3.AppWidgetProvider:
       AppWidgetProvider 除了最常用的onUpdate方法,還有其他的方法,onEnable,onDisabled,onDeleted以及onReceiver,這些方法都會被onReceiver根據Action進行調用:
      onEnable:當該窗口小部件第一次添加到桌面的時候調用該方法,可添加多次但是隻有第一次調用;
      onUpdate:小部件被添加或者第一次更新的時候都會調用一次該方法,小部件的更新機制由updatePeriodMillis來指定;
      onDeleted:每刪除一次小部件都會調用
      onDisabled:當最後一個該類型的小部件會刪除時調用
      onReceiver:廣播內置的方法,用具體事件的分發。
1.4.BroadCastReceiver複習:
       廣播類型分爲標準廣播(異步執行,同時收到、無法截斷)和有序廣播(同步執行、先後順序,可截斷);
1.4.1.接收系統廣播:
        代碼中註冊稱爲動態註冊;在AndroidManifest.xml中註冊稱爲靜態註冊。
        動態註冊監聽網絡變化;步驟一:新建類繼承自BroadCastReceiver,重寫父類的onReceiver方法;具體處理邏輯放在其中;步驟二:onCreate方法中創建IntentFilter實例,系統發出android.net.conn.CONNECTIVITY_CHANGE的廣播,在IntentFilter添加該Action;步驟三:在onCreate方法中使用registerReceiver進行註冊;在onDestroy中使用unregisterReceiver進行取消註冊。步驟四:添加權限:android.permission.ACCESS_NETWORK_STATE。

public class MainActivity extends AppCompatActivity {
    private IntentFilter intentFilter;
    private NetworkChangedReceiver networkChangedReceiver;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        intentFilter = new IntentFilter();
        intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
        networkChangedReceiver = new NetworkChangedReceiver();
        registerReceiver(networkChangedReceiver,intentFilter);
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        unregisterReceiver(networkChangedReceiver);
    }
    class NetworkChangedReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            //getSystemService獲取ConnectivityManager的實例,這是系統服務類,專門用來網絡管理
            ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
            //getActiveNetworkInfo得到NetworkInfo的實例;
            NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
            //isAvailable判斷網絡是否可用
            if(networkInfo!=null &&networkInfo.isAvailable()){
                Toast.makeText(context,"有網",Toast.LENGTH_SHORT).show();
            }else{
                Toast.makeText(context,"沒網",Toast.LENGTH_SHORT).show();
            }
        }
    }
}

       靜態註冊實現開機啓動:動態廣播雖然靈活,但只有在程序啓動之後才能接收到廣播,靜態註冊可以在程序未啓動的情況下接收到廣播。步驟一:右鍵快速新建BroadCastReceiver,在XML中註冊,在onReceiver中寫一個簡單的Toast;步驟二:系統啓動完成之後會發出一條android.intent.action.BOOT_COMPLETED的廣播,需要在<intent-filter>中添加相應Action;步驟三:聲明權限:android.permission.RECEIVE_BOOT_COMPLETED。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.hzk.broadcastreceiver">
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    ....
        <receiver
            android:name=".BootCompleReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
            </intent-filter>
        </receiver>
    </application>
</manifest>
public class BootCompleReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        // TODO: This method is called when the BroadcastReceiver is receiving
        // an Intent broadcast.
        Toast.makeText(context,"啓動成功啦",Toast.LENGTH_SHORT).show();
    }
}

1.4.2.發送自定義廣播:
         發送標準廣播:步驟一:快速新建廣播接收器,複寫onReceiver方法,簡單Toast,在xml中修改receiver: <intent-filter>   <action android:name="com.example.hzk.broadcastreceiver.MyBroadCastReceiver"/>  </intent-filter>;告訴廣播接收器我們需要接受到什麼樣的廣播;步驟二:完成MainActiviy的佈局,Button點擊之後發送標準廣播。

  btn_click.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
 Intent intent = new Intent("com.example.hzk.broadcastreceiver.MyBroadCastReceiver");
                sendBroadcast(intent);
            }
        });

        發送有序廣播;將sendBroadcast(intent);改成 sendOrderedBroadcast(intent,null);即可,可以在onReceiver方法中使用abortBroadcast();來攔截廣播;也可以使用<intent-filter  android:priority="100">設置優先級。
1.4.3.使用本地廣播
       系統全局廣播可被任何程序接收到,不安全,Android引入一套本地廣播機制,使得發出廣播只能在應用程序內部傳遞。而且廣播接收器只能接收到來自本應用程序發出的廣播,安全性問題得以解決。優勢安全高效。
       LocalBroadcastManager的getInstance得到它的一個實例,註冊、註銷都一樣,發出一條com.example.hzk.LOCAL_BROADCAST的廣播。

public class MainActivity extends AppCompatActivity {
    private Button btn_click, btn_nativeclick;
    private IntentFilter intentFilter;
    private NetworkChangedReceiver networkChangedReceiver;
    private LocalReceiver localReceiver;
    private LocalBroadcastManager localBroadcastManager;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        localBroadcastManager = LocalBroadcastManager.getInstance(this);//獲取實例
        btn_nativeclick.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent("com.example.hzk.LOCAL_BROADCAST");
                localBroadcastManager.sendBroadcast(intent);//發送本地廣播
            }
        });
        intentFilter.addAction("com.example.hzk.LOCAL_BROADCAST");
        localReceiver = new LocalReceiver();
        localBroadcastManager.registerReceiver(localReceiver,intentFilter);
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        localBroadcastManager.unregisterReceiver(localReceiver);
    }
    class LocalReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            Toast.makeText(context, "received local receiver", Toast.LENGTH_LONG).show();
        }
    }
}

1.5.PendingIntent概述
       pendingIntent與Intent的區別:PendingIntent是在將來某個不確定的時刻發生;而Intent(意圖)是立刻發生。典型應用場景是給RemoteViews添加點擊事件。RemoteViews運行於遠程進程中,不像View可以setOnClickListener設置點擊事件。PendingIntent通過send和cancel方法來發送和取消特定的待定Intent。
       PendingIntent支持三種特定意圖:啓動Activity、Service和BroadCastReceiver。其中方法中第一個和第三個參數容易理解,第二個是發送方請求碼,多數爲零。第四個標誌位:一般使用FLAG_UPDATE_CURRENT。
       PendingIntent的匹配規則爲:如果兩個PendingIntent他們內部的Intent相同並且requstCode也相同的話,那麼PendingIntent就是相同的,Intent的匹配規則是:只要Intent之間的ComponentName和intent-filter相同,那麼這兩個intent就相同,需要注意的是Extras不參與匹配過程,只要intent之間的name和intent-filter相同就行。

                      
(二)RemoteViews的內部機制
2.1.RemoteViews支持的類型及相關set的API

      RemoteViews的作用在其他進程中顯示並且更新View的界面,爲了更好的理解他的內部機制。其構造方法爲 public RemoteViews(String packageName, int layoutId);第一個表示當前的包名,第二個是加載的佈局,RemoteViews目前並不能支持所有的View類型。Layout包括FrameLayout、LinearLayout、RelativeLayout、GridLayout;View 包括ButtonImageButton,ImageView,ProgressBar,TextView,ListView,GridView,stackView,AdapterViewFlipper,ViewStub等。譬如RemoteViews無法使用EditText,報錯。
       RemoteViews也沒有提供findViewById方法,因此無法直接訪問裏面的View元素,而必須通過RemoteViews所提供的一系列set方法來完成,當然這是因爲RemoteViews在遠程進程中顯示。

               

RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.notification_item);
//viewId是被操作的id,setText是一個方法名,text是給TextView要設置的文本
remoteViews.setTextViewText(R.id.tv_title, "This is Title");
remoteViews.setTextViewText(R.id.tv_content, "This is Notification Content");
remoteViews.setImageViewResource(R.id.iv_img, R.mipmap.ic_launcher);
remoteViews.setOnClickPendingIntent(R.id.ll_open, pendingIntent);

2.2.RemoteViews的內部機制
       RemoteViews主要用於通知欄和通知欄和桌面小部件,通知欄和小部件分別由NotificationManager和AppWidgetProvider管理。而NotificationManager和AppWidgetProvider通過Binder分別爲SystemService進程中的NotificationManagerService和AppWidgetService。與我們的進程構成跨進程通信。
       系統完全可以通過Binder去支持所有的View和View操作,但是這樣做的話代價太大。步驟一:系統首先將Vew操作封裝裝到Action對象並將這些對象通過Binder跨進程傳輸到遠程進程,步驟二:在遠程進程中執行Action對象中的具體操作。Action同樣實現了Parcelable接口。eg:當我們通過NotificationManager和AppWidgetManager來提交我們的更新時,這些Action對象就會傳輸到遠程進程並在遠程進程中依次執行。遠程進程通過RemoteViews的apply方法來進行View的更新操作。優勢:無需大量Binder,批量進行,避免大量IPC操作。

                             
       Action對象的apply方法是真正操作View的,每一次的set操作都會對應着它裏面的Action對象,將其添加進ArrayList列表中;performApply的作用是遍歷mActions列表並執行每一個Action對象的apply方法。
       ReflectionAction 表示的是一個反射的動作,他通過對View的操作會以反射的方式調用,其中getMethod就是根據方法名來反射所需要的方法。Set方法包括了setTextViewText、setBoolean等。
2.3.setOnClickPendingIntent,setPendingIntentTemplate,以及setonClickFillinIntent的區別
       第一個用於給普通view設置單擊事件;但不能給集合(ListView和StackView)中的View設置單擊事件,比如ListView中的item不能通過setOnClickPendingIntent添加單擊事件。其次,如果要對ListView和StackView設置點擊事件,則需要將後兩者組合使用。因爲開銷大。
(三)RemoteViews的意義
     場景:打造一個模擬通知欄效果以實現跨進程UI更新。
     解決:B用來不停發送通知欄信息,A顯示這個RemoteViews對象。
     B的代碼:構造RemoteViews對象並將其傳輸給A。

public void onButtonClick(View v) {
        //步驟一:新建RemoteViews對象,並反射調用設置文本內容、圖片資源
        RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_simulated_notification);
        remoteViews.setTextViewText(R.id.msg, "msg from process:" + Process.myPid());
        remoteViews.setImageViewResource(R.id.icon, R.drawable.icon1);
        //步驟二:構建待定意圖:getActivity實現啓動Activity企圖,設置點擊事件Intent。
        PendingIntent pendingIntent = PendingIntent.getActivity(this,
                0, new Intent(this, DemoActivity_1.class), PendingIntent.FLAG_UPDATE_CURRENT);
        PendingIntent openActivity2PendingIntent = PendingIntent.getActivity(
                this, 0, new Intent(this, DemoActivity_2.class), PendingIntent.FLAG_UPDATE_CURRENT);
        remoteViews.setOnClickPendingIntent(R.id.item_holder, pendingIntent);
        remoteViews.setOnClickPendingIntent(R.id.open_activity2, openActivity2PendingIntent);
        //步驟三:以廣播的形式發送Intent
        Intent intent = new Intent(MyConstants.REMOTE_ACTION);
        intent.putExtra(MyConstants.EXTRA_REMOTE_VIEWS, remoteViews);
        sendBroadcast(intent);
}

        A的代碼:接收B中的廣播並顯示RemoteViews即可。

public class MainActivity extends Activity {
    private LinearLayout mLinearLayout;
    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            //從Intent中取出RemoteViews對象
            RemoteViews remoteViews = intent.getParcelableExtra("send_bro");
            if (remoteViews != null) {
                updateUI(remoteViews);
            }
        }
    };
    //通過apply方法加載佈局並且執行更新操作,最後將得到的View添加到A的佈局中即可
    private void updateUI(RemoteViews remoteViews) {
        View view = remoteViews.apply(this, mLinearLayout);
        mLinearLayout.addView(view);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_a);

        initView();
    }
     //註冊廣播,初始化視圖
    private void initView() {
        mLinearLayout = (LinearLayout) findViewById(R.id.mLinearLayout);
        IntentFilter intent = new IntentFilter("send_bro");
        registerReceiver(mBroadcastReceiver, intent);
    }
   //銷燬時解除廣播
    @Override
    protected void onDestroy() {
        super.onDestroy();
        unregisterReceiver(mBroadcastReceiver);
//A和B屬於不同應用,那麼B中的佈局文件的資源id傳輸到A中以後很有可能是無效的
//就通過資源名稱來加載佈局文件。首先兩個應用要提前約定好RemoteViews中的佈局的文件名稱,比如“layout simulated notification”,然後在A中根據名稱找到並加載。
//        int layoutId = getResources().getIdentifier("layout_simulated_notification","layout",getPackageName());
//        View view = getLayoutInflater().inflate(layoutId,mLinearLayout,false);
//        remoteViews.reapply(this,view);
//        mLinearLayout.addView(view);
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章