
上手了Android N Preview,第一個不能錯過的新特性就是App分屏的支持。Android7.0原生系統就可以支持兩個App橫屏並排或者豎屏上下擺放了。第二個新特性就是在Android TV上,原生系統也可以支持App實現畫中畫,用戶可以一邊看視頻一邊操作其他的應用。
其實早先在國內部分廠商以及鵝廠的微信App就已經支持在大尺寸手機上進行分屏瀏覽。也有一些視頻播放器,如MoboPlayer就已經實現了按下Home鍵回到首頁時以迷你播放器的形式進行播放。這種體驗非常棒,我猜測一般是通過WindowManager來添加懸浮播放器界面的。但是這次是原生系統增加了對這種特性的支持,相信我們會有更多理由為用戶的體驗做出更多新的嘗試。
下面介紹一下我參考multi-window-support對App分屏模式進行的實踐。
首先引用一下官方的說法:
如果你使用Android N Preview SDK來構建你的App,你可以給添加App一些分屏瀏覽的配置。例如設置Activity的最小尺寸,也可以禁止自己的App進入分屏模式,保證你的App只能在全屏模式下展示。
概述
Android N允許用戶一次在屏幕中使用兩個App,例如將屏幕一分為二,左邊瀏覽網頁,右邊查看郵件。具體的體驗取決於你的設備。
- 手持設備中,用戶可以左右並排/上下擺放兩個App來使用。用戶還可以左右/上下拖拽中間的那個小白線來改變兩個App的尺寸。
-
在運行Android N的Nexus Player上,App可以實現畫中畫模式,允許用戶使用一個App瀏覽內容的同時,在另一個App上操作。
-
大尺寸設備的廠商甚至可以實現自由模式,這樣就可以使得用戶可以完全自由地改變界面的尺寸。這又是與分屏更為不同一種體驗。
用戶是如何操作來進入分屏模式的呢:
- 點擊右下角的方塊,進入任務管理器,長按一個App的標題欄,將其拖入屏幕的高亮區域,這個App金進入了分屏模式。然后在任務管理器中選擇另一個App,單擊它使得這個App也進入分屏模式。
- 打開一個App,然后長按右下角的方塊,此時已經打開的這個App將進入分屏模式。然后在屏幕上的任務管理器中選擇另外一個App,單擊它使得這個App也進入分屏模式。
- 最新發現:下拉通知欄,長按右上角的設置圖標,將開啟隱藏設置功能
“系統界面調諧器”
,進入設置界面,最下方有系統界面調諧器選項,進入后選擇“Other”->“啟用分屏上滑手勢”
,就可以從任務管理器上上滑進入分屏模式了。具體操作是當一個App已經處於全屏模式時,用手指從右下角的小方塊向上滑動
。這個設置將來在正式版可能有變化,所以還是不要太依賴。
用戶還可以在這兩個App之間拖動數據,例如將一個App的Activity上的文件拖動到另外一個App的Activity中去。具體的實現下面會介紹,谷歌官方也有拖拽相關的教程。
分屏模式的生命周期
首先要說明的一點是,分屏模式沒有改變Activity的生命周期。
官方說法是: > 在分屏模式下,用戶最近操作、激活過的Activity將被系統視為topmost
。而其他的Activity都屬於paused
狀態,即使它是一個對用戶可見的Activity。但是這些可見的處於paused
狀態的Activity將比那些不可見的處於paused
狀態的Activity得到更高優先級的響應。當用戶在一個可見的paused
狀態的Activity上操作時,它將得到恢復resumed
狀態,並被系統視為topmost
。而之前那個那個處於topmpst
的Activity將變成paused
狀態。
怎么理解這段話,看下圖:
其實就是說處於分屏模式下的兩個Ap各自處於生命周期的什么狀態。上圖中我打開了兩個App,上面的是一個Gmail App,下面這個是一個Demo App(ApkParser先感謝作者的分享~)是個開源應用,能夠解析Apk,后面會用到它)。現在這兩個App都是進入了分屏模式
,我們還可以拖動中間這條白線來調整兩個App占用的大小。
我點擊了Gmail,瀏覽了一封郵件,那么此時Gmail就被系統視為topmost
狀態,它是處於resumed
狀態的,而下面的ApkPaserDemo雖然對用戶可見,但是它仍然是處於paused
狀態的。接着我點擊了系統的back
按鈕返回,響應的是上面的Gmail(因為它被視為topmost)。然后我又點擊了下面的ApkParserDemo,這時它從paused
狀態變成了resumed
狀態。而上面的Gmail進入了 paused
狀態。
注意,這兩個App對於用戶都是始終可見的,當它們處於paused
狀態時,也將比那些后台的處於不可見的App得到更高系統優先級。這個優先級怎么體現呢?兩個App進入分屏模式后,一定有一個處於resume/topmost狀態,假如我一直按back
返回,當這個topmost狀態App的task返回棧已經為空時,那么系統將把另外一個可見的App恢復為全屏模式,這就是我的理解。
那么這種可見的pause
的狀態將帶來什么影響呢?引用下官方說法是:
在分屏模式中,一個App可以在對用戶可見的狀態下進入
paused
狀態,所以你的App在處理業務時,應該知道自己什么時候應該真正的暫停
。例如一個視頻播放器,如果進入了分屏模式,就不應該在onPaused()
回調中暫停視頻播放,而應該在onStop()
回調中才暫停視頻,然后在onStart
回調中恢復視頻播放。關於如果知道自己進入了分屏模式,在Android N
的Activity類中,增加了一個void onMultiWindowChanged(boolean inMultiWindow)
回調,所以我們可以在這個回調知道App是不是進入了分屏模式。
當App進入分屏模式后,將會觸發Activity的onConfigurationChanged()
,這與以前我們在處理App從橫豎屏切換
時的方法一樣,不同於的是這里是寬/高有所改變,而橫豎屏切換
是寬高互換。至於如何處理,可以參考官方文檔處理運行時變更。我們最好處理好這種運行時狀態的改變,否則我們的Activity將被重新創建,即以新的寬高尺寸重新onCreate()
一遍。
注意,如果用戶重新調整窗口的大小,系統在必要的時候也可能觸發onConfigurationChanged()
。當App的窗口被用戶拖動,其尺寸改變后界面的還沒有繪制完成時,系統將用App主題中的windowBackground
屬性指定的背景來暫時填充這些區域。
如何設置App的分屏模式
說了一堆分屏的操作方法、生命周期,那么作為開發者,怎樣才能讓App進入分屏
模式呢?有下面這幾個屬性。
android:resizeableActivity
如果你適配到了Android N
,即build.gradle
是這樣的:
1 android { 2 compileSdkVersion 'android-N' 3 buildToolsVersion '24.0.0 rc1' 4 5 defaultConfig { 6 applicationId "com.example.noughtchen.andndemo" 7 minSdkVersion 'N' 8 targetSdkVersion 'N' 9 versionCode 1 10 versionName "1.0" 11 } 12 ... 13 }
那么直接在AndroidManifest.xml
中的<application>
或者<activity>
標簽下設置新的屬性android:resizeableActivity="true"
。
設置了這個屬性后,你的App/Activity就可以進入分屏模式
或者自由模式
了。
如果這個屬性被設為false
,那么你的App將無法進入分屏模式,如果你在打開這個App時,長按右下角的小方塊,App將仍然處於全屏模式,系統會彈出Toast提示你無法進入分屏模式。這個屬性在你target到Android N
后,android:resizeableActivity
的默認值就是true
。
注意:假如你沒有適配到Android N(
targetSDKVersion < Android N
),打包App時的compileSDKVersion < Android N
,你的App也是可以支持分屏的!!!!原因在於:如果你的App沒有 設置僅允許Activity豎屏/橫屏
,即沒有設置類型android:screenOrientation="XXX"
屬性時,運行Android N系統的設備還是 可以 將你的App 分屏!! 但是這時候系統是不保證運行時的穩定性的,在進入分屏模式時,系統首先也會彈出Toast來提示你說明這個風險。所以其實我們在視頻里看到那么多系統自帶的App都是可以分屏瀏覽,原因就在於此。這些App其實也並沒有全部適配到Android N。我不是騙你,不信你用
ApkParser
打開前面分屏過Gmail App的xml文件看看!
android:supportsPictureInPicture
這里不多說,Activity標簽下,添加android:supportsPictureInPicture="true"
即可。
1 <activity 2 android:name=".MainActivity" 3 android:label="@string/app_name" 4 android:resizeableActivity="true" 5 android:supportsPictureInPicture="true" 6 android:theme="@style/AppTheme.NoActionBar"> 7 ... 8 </activity>
Layout attributes
在Android N中,我們可以向manifest
文件中添加layout
節點,並設置一些新增加的屬性,通過這些屬性來設置分屏模式的一些行為,如最小尺寸等。
- android:defaultWidth
- android:defaultHeight
- android:gravity
- android:minimalSize
我們可以給一個Activity
增加一個layout
子節點:
1 <activity 2 android:name=".MainActivity" 3 android:label="@string/app_name" 4 android:resizeableActivity="true" 5 android:supportsPictureInPicture="true" 6 android:theme="@style/AppTheme.NoActionBar"> 7 ... 8 <layout android:defaultHeight="500dp" 9 android:defaultWidth="600dp" 10 android:gravity="top|end" 11 android:minimalSize="450dp" /> 12 ... 13 </activity>
作為開發者,我們應該如何讓自己的App進入分屏模式,當App進入分屏模式時,我們注意哪些問題。
簡單地說,我認為除了保證分屏時App功能、性能正常以外,我們需要重點學習如何在分屏模式下打開新的Activity以及如何實現跨APP/Activity的拖拽功能
用分屏模式運行你的App
Android N中新增了一些方法來支持App的分屏模式。同時在分屏模式下,也禁用了App一些特性。
分屏模式下被禁用的特性
- 自定義系統UI,例如分屏模式下無法隱藏系統的狀態欄。
- 無法根據屏幕方向來旋轉App的界面,也就是說
android:screenOrientation
屬性會被系統忽略。
分屏模式的通知回調、查詢App是否處於分屏狀態
最新的Android N SDK中Activity類中增加了下面的方法。
- inMultiWindow():返回值為boolean,調用此方法可以知道App是否處於分屏模式。
- inPictureInPicture():返回值為boolean,調用此方法可以知道App是否處於畫中畫模式。
注意:畫中模式其實是一個特殊的分屏模式
,如果mActivity.inPictureInPicture()
返回true
,那么mActivity.inMultiWindow()
一定也是返回true
。
- onMultiWindowChanged(boolean inMultiWindow):當Activity進入或者退出分屏模式時,系統會回調這個方法來通知開發者。回調的參數
inMultiWindow
為boolean類型,如果inMultiWindow
為true,表示Activity進入分屏模式;如果inMultiWindow
為false,表示退出分屏模式。 - onPictureInPictureChanged(boolean inPictureInPicture):當Activity進入畫中畫模式時,系統會回調這個方法。回調參數
inPictureInPicture
為true
時,表示進入了畫中畫模式;inPictureInPicture
為false
時,表示退出了畫中畫模式。
Fragment
類中,同樣增加了以上支持分屏模式的方法,例如Fragment.inMultiWindow()
。
如何進入畫中畫模式
調用Activity
類的enterPictureInPicture()
方法,可以使得我們的App進入畫中畫模式。如果運行的設備不支持畫中畫模式,調用這個方法將不會有任何效果。更多畫中畫模式的資料,請參考picture-in-picture。
在分屏模式下打開新的Activity
當你打開一個新的Activity時,只需要給Intent添加Intent.FLAG_ACTIVITY_LAUNCH_TO_ADJACENT
,系統將嘗試將它設置為與當前的Activity共同以分屏的模式顯示在屏幕上。
注意:這里只是嘗試,但這不一定是100%生效的,前一篇博客里也說過,假如新打開的Activity的android:resizeableActivity
屬性設置為false
,就會禁止分屏瀏覽這個Activity。所以系統只是嘗試去以分屏模式打開一個新的Activity,如果條件不滿足,將不會生效!此外,我實際用Android N Preview SDK
實踐的時候發現這個FLAG
實際得值是FLAG_ACTIVITY_LAUNCH_ADJACENT
,並非是FLAG_ACTIVITY_LAUNCH_TO_ADJACENT
。
當滿足下面的條件,系統會讓這兩個Activity進入分屏模式:
- 當前Activity已經進入到分屏模式。
- 新打開的Activity支持分屏瀏覽(即android:resizeableActivity=true)。
此時,給新打開的Activity,設置intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT | Intent.FLAG_ACTIVITY_NEW_TASK);
才會有效果。
建議參考官方的Sample:MultiWindow Playground Sample
那么為何還需要添加FLAG_ACTIVITY_NEW_TASK
?看一下官方解釋:
注意:在同一個Activity返回棧中,打開一個新的Activity時,這個Activity將會繼承上一個Activity所有和分屏模式
有關的屬性。如果你想要在一個獨立的窗口以分屏模式打開一個新的Activity,那么必須新建一個Activity返回棧。
此外,如果你的設備支持自由模式
(官方名字叫freeform,暫且就這么翻譯它,其實我認為這算也是一種尺寸更自由的分屏模式,上一篇博客里提到過如果設備廠商支持用戶可以自由改變Activity的尺寸,那么就相當於支持自由模式
,這將比普通的分屏模式更加自由),打開一個Activity時,還可通過ActivityOptions.setLaunchBounds()
來指定新的Activity的尺寸和在屏幕中的位置。同樣,這個方法也需要你的Activity已經處於分屏模式時,調用它才會生效。
支持拖拽
在上面介紹里也提到過,現在我們可以實現在兩個分屏模式的Activity之間拖動內容了。Android N Preview SDK中,View
已經增加支持Activity之間拖動的API。具體的類和方法,可以參考N Preview SDK Reference,主要用到下面幾個新的接口:
- View.startDragAndDrop():View.startDrag() 的替代方法,需要傳遞
View.DRAG_FLAG_GLOBAL
來實現跨Activity拖拽。如果需要將URI權限傳遞給接收方Activity,還可以根據需要設置View.DRAG_FLAG_GLOBAL_URI_READ
或者View.DRAG_FLAG_GLOBAL_URI_WRITE
。 - View.cancelDragAndDrop():由拖拽的發起方調用,取消當前進行中的拖拽。
- View.updateDragShadow():由拖拽的發起方調用,可以給當前進行的拖拽設置陰影。
- android.view.DropPermissions:接收方App所得到的權限列表。
- Activity.requestDropPermissions():傳遞URI權限時,需要調用這個方法。傳遞的內容存儲在DragEvent中的ClipData里。返回值為前面的
android.view.DropPermissions
。
下面是我自己寫的一個demo,實現了在分屏模式下,把一個Activity中ImageView中保存的內容到另外一個Activity中進行顯示。實際應用中,可以還可以傳遞圖片的url或者Bitmap對象。
上圖是一個最基本的例子,實現了把MainActivity中的圖片保存的內容,拖拽到SecondActivity中。實現步驟如下:
在MainActivity中,發起拖拽。
1 // 1.首先我們在分屏模式下,打開自己App中的SecondActivity 2 findViewById(R.id.launch_second_activity).setOnClickListener(new View.OnClickListener() { 3 @Override 4 public void onClick(View view) { 5 Intent intent = new Intent(MainActivity.this, SecondActivity.class); 6 intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT | Intent.FLAG_ACTIVITY_NEW_TASK); 7 startActivity(intent); 8 } 9 }); 10 11 // 2.然后我們在MainActivity中發出拖拽事件 12 imageView = (ImageView) findViewById(R.id.img); 13 /** 拖拽的發送方Activity和ImageView */ 14 imageView.setTag("I'm a ImageView from MainActivity"); 15 imageView.setOnTouchListener(new View.OnTouchListener() { 16 17 public boolean onTouch(View view, MotionEvent motionEvent) { 18 if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { 19 /** 構造一個ClipData,將需要傳遞的數據放在里面 */ 20 ClipData.Item item = new ClipData.Item((CharSequence) view.getTag()); 21 String[] mimeTypes = {ClipDescription.MIMETYPE_TEXT_PLAIN}; 22 ClipData dragData = new ClipData(view.getTag().toString(), mimeTypes, item); 23 View.DragShadowBuilder shadow = new View.DragShadowBuilder(imageView); 24 /** startDragAndDrop是Android N SDK中的新方法,替代了以前的startDrag,flag需要設置為DRAG_FLAG_GLOBAL */ 25 view.startDragAndDrop(dragData, shadow, null, View.DRAG_FLAG_GLOBAL); 26 return true; 27 } else { 28 return false; 29 } 30 } 31 });
在SecondActivity
中,接收這個拖拽的結果,在ACTION_DROP
事件中,把結果顯示出來。
1 dropedText = (TextView) findViewById(R.id.text_drop); 2 dropedText.setOnDragListener(new View.OnDragListener() { 3 @Override 4 public boolean onDrag(View view, DragEvent dragEvent) { 5 switch (dragEvent.getAction()) { 6 case DragEvent.ACTION_DRAG_STARTED: 7 Log.d(TAG, "Action is DragEvent.ACTION_DRAG_STARTED"); 8 break; 9 10 case DragEvent.ACTION_DRAG_ENTERED: 11 Log.d(TAG, "Action is DragEvent.ACTION_DRAG_ENTERED"); 12 break; 13 14 case DragEvent.ACTION_DRAG_EXITED: 15 Log.d(TAG, "Action is DragEvent.ACTION_DRAG_EXITED"); 16 break; 17 18 case DragEvent.ACTION_DRAG_LOCATION: 19 break; 20 21 case DragEvent.ACTION_DRAG_ENDED: 22 Log.d(TAG, "Action is DragEvent.ACTION_DRAG_ENDED"); 23 break; 24 25 case DragEvent.ACTION_DROP: 26 Log.d(TAG, "ACTION_DROP event"); 27 /** 3.在這里顯示接收到的結果 */ 28 dropedText.setText(dragEvent.getClipData().getItemAt(0).getText()); 29 break; 30 31 default: 32 break; 33 } 34 35 return true; 36 } 37 });
這里實現的關鍵在新增加的startDragAndDrop
方法,看下官方的API文檔:
清楚地提到了,發出的DragEvent能夠被所有可見的View對象接收到
,所以在分屏模式下,SecondActivity可以監聽View的onDrag事件,於是我們監聽它!
接着,我們看下DragEvent.ACTION_DROP
事件發生的條件:
當被拖拽的View的陰影進入到接收方View的坐標區域,如果此時用戶松手,那么接收方View就可以接收到這個Drop事件。一目了然,我們通過拖拽ImageView到圖上的灰色區域,松手,便可以觸發DragEvent.ACTION_DROP
,把數據傳到SecondActivity中了。
其實還有更復雜的一些情況,需要調用requestDropPermissions
,后續我再進一步實踐一下。
這個demo的地址在這里,先分享出來,后面我再接着完善它。
在分屏模式下測試你的App
無論你是否將自己的App適配到了Android N,或者是支持分屏模式,都應該找個Android N的設備,來測試一下自己的App在分屏模式下會變成什么樣。
設置你的測試設備
如果你有一台運行Android N的設備,它是默認支持分屏模式的。
如果你的App不是用Android N Preview SDK打包的
如果你的App是用低於Android N Preview SDK
打包的,且你的Activity支持橫豎屏切換
。那么當用戶在嘗試使用分屏模式時,系統會強制將你的App進入分屏模式。(我在第一篇博客里提到過這個,Android N Preview的介紹視頻中,很多Google家的App都可以進入分屏模式,但是打開它們的xml一看,其實targetSDKVersion = 23
)
因此,如果你的App/Activity支持橫豎屏切換,那么你應該嘗試一下讓自己的App分屏,看看當系統強制改變你的App尺寸時,用戶是否還可以接受這種體驗。如果你的App/Activity不支持橫豎屏切換,那么你可以確認一下,看看當嘗試進入分屏時,你的App是不是仍然能夠保持全屏模式。
如果你給App設置了支持分屏模式
如果你使用了Android N Preview SDK
來開發自己的App,那么應該按照下面的要點檢查一下自己的App。
- 啟動App,長按系統導航欄右下角的小方塊(Google官方把這個叫做Overview Button),確保你的App可以進入分屏模式,且尺寸改變后仍然能正常工作。
- 啟動任務管理器(即單擊右下角的小方塊),然后長按你App的標題欄,將它拖動到屏幕上的高亮區域。確保你的App可以進入分屏模式,且尺寸改變后仍然能正常工作。
這兩點在上面介紹過,讓自己的App進入分屏模式有三種方法。第三種方法,就是在打開自己的App時,用手指從右下角的小方塊向上滑動,這樣也可以使得正在瀏覽的App進入分屏模式。這種方法目前屬於實驗性功能,正式版不一定保留。
- 當你的App進入分屏后,通過拖動兩個App中間的分欄上面的小白線,從而改變App的尺寸,觀察App中各個UI元素是否正常顯示。
- 如果你給自己的App/Activity設置了最小尺寸,可以嘗試在改變App尺寸時,低於這個最小尺寸,觀察App是不是會回到設定好的最小尺寸。
- 在進行上面幾項測試時,請同時驗證自己的App功能和性能是否正常,並注意一下自己的App在更新UI時是否花費了太長的時間。
這幾項測試,其實主要強調的是,我們的App可以順利的進入/退出分屏模式,且改變App的尺寸時,UI依然可以也非常順滑。
這里我想多說一句,如果進入了分屏模式,要注意下App彈出的對話框,因為屏幕被兩個App分成兩塊之后,對話框也是可以彈出兩個的。這時對話框上的UI元素可能就會變得比較小了,如果我們的代碼是寫死的大小,例如對話框是一個WebView,就需要特別注意了,搞不好顯示出來就缺了一塊了,這里需要我們做好適配。
測試清單
關於功能、性能方面測試,還可以按照下面的操作來進行。
- 讓App進入,再退出分屏模式,確保此時App功能正常。
- 讓App進入分屏模式,激活屏幕上的另外一個App,讓自己的App進入
可見、paused
狀態。舉了例子來講,如果你的App是一個視頻播放器,那么當用戶點擊了屏幕上另外一個App時,你的App不應該停止播放視頻,即使此時你的Activity/Fragment已經接到了onPaused()
回調。 - 讓App進入分屏模式,拖動分欄上的小白線,改變App的尺寸。請在豎屏(兩個App一上一下布局)和橫屏(兩個App一左一右布局)模式下分別進行改變尺寸的操作。確保App不會崩潰,各項功能正常,且UI的刷新沒有花費太多時間。
- 在短時間內、多次、迅速地改變App尺寸,確保App沒有崩潰,且沒有發生內存泄露。關於內存使用方面的更詳細注意事項,請參考Investigating Your RAM Usage。
- 在不同的窗口設置的情況下,正常使用App,確保App功能正常,文字仍然可讀,其他的UI元素也沒有變得太小,用戶仍然可以舒適地操作App。
這幾項測試,其實主要是說當App在分屏模式下運行時,仍然可以保持性能的穩定,不會Crash也不會OOM。
如果你給App設置了禁止分屏模式
如果你給App/Activity設置了android:resizableActivity="false"
,你應該試試當用戶在Android N的設備上,嘗試分屏瀏覽你的App時,它是否仍然能保持全屏模式。
以上就是參考Google最新的multi-window進行的實踐,總結下,我認為有3點比較重要:
- 如何讓自己的App/Activity順利的進入和退出分屏模式,可以參考處理運行時改變這一章。
- 如何在分屏模式下打開新的Activity,可以參考Google官方的MultiWindow Playground Sample。
- 如何實現跨App/Activity的拖拽功能,可以參考Drag and Drop這一章。