开始的开始
自从我的Nexus 6加入了Android beta计划以来,我便在很早的时候就体验上了Android Nougat的一些新特性,自然也体验到了比较重要的Multi-Window
新特性。不过,在当前Android Nougat普及度特别低(尤其是我的Nexus 6还没有收到官方ota)的情况下,Multi-Window
的体验也大打了不少折扣。
根据民间的说法,Multi-Window
可以同时在屏幕上打开两个应用,你可以在用其中一个App的同时看到另一个App的内容。想象一下,你正在玩游戏或者看电影,这时你可能同时正在和朋友聊天,这在以往,你可能需要整屏切来切去,现在,你可以在同一个屏幕上完成这两件事情,而且互不耽误。再想象一下,你正在和朋友探讨知识,突然遇到了一个你讲不清楚的原理,这时你开了一个分屏打开了Google,这边Google一下答案,复制一下,然后立马在那边继续你们的探讨,这大大加快了你获取知识的效率,同时也为装逼提供了可能。
经过初步的探索,我发现Multi-Window
的分屏并不是以App为基本单位,确切地说,是以Activity
作为基本单位。这就意味着,同一个App的不同的Activity,也可以共享屏幕。再精确地说,我们可以开发出一款App,这个App可以打开两个窗口分享整个屏幕,每个窗口加载一个不同的Activity。想象一下,如果有一个邮件App,可以左边窗口查看收件箱,右边窗口写新邮件,这可能会方便很多。再想象一下,如果有一个象棋游戏,你可以左边窗口扮演玩家A,右边窗口扮演玩家B,自己和自己下棋。哦剩下的脑洞交给你们了。
其实当我首次看到Multi-Window
的介绍时,我不认为这个特性会带来多少方便,因为我感觉手机屏幕本来就比较小,再分屏就有点施展不开了;这个特性相对地对于平板更好一些。直到我看到Android里面还有一个叫做Drag and Drop
的东西,这让我感觉Multi-Window
能多多少少施展一些作用了。
Drag and Drop
让我们可以把数据从一个View拖到另一个View。比方说,我们有两个EditText,其中在第一个EditText输入了某项内容,然后我们用手指从第一个EditText拖动到第二个EditText,然后第二个EditText就自动填充了第一个EditText的内容,是不是很好玩?这里传递的数据可以是任何我们需要的数据,而View是任何我们可以操作的控件,这就为我们的交互设计提供了更多的想象空间。更重要的,Drag and Drop
很好地支持了Multi-Window
,这为跨窗口数据分享提供了可能。
这里我实现了这样一个App:这个App启动了两个Activity进入分屏模式,第一个Activity有一个输入URL的EditText,第二个Activity有一个WebView。我们在第一个Activity中输入了URL之后,用手从EditText跨窗口拖动到第二个Activity的WebView上,就直接在WebView中打开我们填写的URL。
下面我们来一步一步来探索一下这些具体是怎么完成的。这个工程我放在了Github-MultiWindowGiraffe,以供参考。
Multi-Window
在Android Nougat上进入Multi-Window
模式的方法可以参照Multi-Window Support上的说明,这里只从开发层面加以描述。
当App进入Multi-Window
模式时,系统会向Activity发送一个configuration change的通知,其对Activity生命周期的影响和屏幕旋转是一样的。当处于Multi-Window
模式时,我们可以看到两个Activity分享屏幕,其中用户正在操作的Activity处于Active
状态,另一个处于Paused
状态。用户的操作从其中一个Activity转到另一个Activity时,会调用原来Activity的onPause()
方法,同时会触发新操作的Activity的onResume()
方法。
因此,这里出现了一个处于Paused
状态但同时又对用户可见的Activity。我们知道,当Activity对用户不可见时,会调用生命周期的onPause()
和onStop()
方法,我们可以在这两个方法里面做一些类似停止视频播放的操作。但是在这里,当分屏的两个Activity之间切换时,只会触发onPause()
方法,如果我们想要用户在另一个窗口操作的时候视频扔继续播放,就只能将停止播放的动作放在onStop()
方法里,不能放到onPause()
方法里了。
Multi-Window配置
要让我们的App增加对Multi-Window
的支持,我们需要在<activity>
或者<application>
标签里增加如下配置:
android:resizeableActivity=["true" | "false"]
如果target API level是24,则该项默认为true。
在Android 7.0,我们可以在manifest中为<activity>
增加<layout>
元素,用来规定Activity在Multi-Window
下的行为。这里可以对尺寸、位置做一些配置。比如可以这样配置:
<activity android:name=".MyActivity">
<layout android:defaultHeight="500dp"
android:defaultWidth="600dp"
android:gravity="top|end"
android:minHeight="450dp"
android:minWidth="300dp" />
</activity>
Multi-Window API支持
Activity对Multi-Window
提供了如下方法:
isInMultiWindowMode()
:查看Activity是否正处于Multi-Window
模式。onMultiWindowModeChanged()
:当Activity进入或者退出Multi-Window
模式时的回调。
我们可以在Activity中根据自己的业务逻辑灵活使用这两个方法。
除此之外,我们还可以以Multi-Window
模式启动一个Activity。想要实现这一点,我们需要在Intent中增加FLAG_ACTIVITY_LAUNCH_ADJACENT
。当启动Activity的Intent包含该Flag时,系统会执行如下动作:
如果设备当前正处于
Multi-Window
模式,则新启动的Activity会占用另外一个窗口,与当前Activity分屏占用整个屏幕。这里需要注意,新的Activity需要以NEW_TASK
的模式启动。如果设备没有处于
Multi-Window
模式,则该Flag无效。
在MultiWindowGiraffe中,我创建了两个Activity:MainActivity
和WebActivity
。当处于分屏模式下,我们期待从MainActivity
启动WebActivity
,使得两个Activity分享整个屏幕,这里可以这样实现:
Intent intent = new Intent(MainActivity.this, WebActivity.class);
intent.putExtra(WebActivity.FIELD_URL, url);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (isInMultiWindowMode()) {
// launch this activity in another split window next to the current one
intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT);
}
startActivity(intent);
这样一来,我们已经可以在同一个屏幕上同时显示两个Activity了。效果如下:
下面,我们期待可以用手指将上面这个EditText
里的URL拖动到下面的WebView
,并且自动加载该URL页面。
Drag and Drop
通过Drag and Drop
,我们可以将数据从一个View传递到另一个View。当用户做出一些我们可以识别的手势操作时(比如长按),我们需要告知系统开始一个Drag
过程。当一个Drag
过程开始后,我们可以为拖动的View生成一个虚拟的阴影,这个阴影可以随着用户的手指进行移动。在移动过程中,系统不断给我们先前设置的Drag Listener
发送一系列事件,我们可以通过不同的事件进行不同的处理。当用户手指离开屏幕时(我们称为Drop
操作),整个Drag and Drop
过程结束。
我们可以通过View.startDragAndDrop方法开始一个Drag过程。该方法的定义为:
boolean startDragAndDrop (ClipData data,
View.DragShadowBuilder shadowBuilder,
Object myLocalState,
int flags)
这四个参数意义分别为:
data
:此次drag包含的数据,可以看到,这里采用了ClipData数据类型。shadowBuilder
:一个DragShadowBuilder
,用来生成drag时指示控件移动的阴影。myLocalState
:一个包含local数据的对象。暂时不用。flags
:Drag
过程的flag配置。
在这里,我们将URL包装成一个ClipData
对象,并且为了支持跨窗口Drag
,我们需要将flag置成View.DRAG_FLAG_GLOBAL
:
// create ClipData Object
ClipData.Item item = new ClipData.Item(mUrlEditText.getText().toString());
ClipData data = new ClipData("LABEL", new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}, item);
// start drag
view.startDragAndDrop(data, new GiraffeDragShadowBuilder(view), null, View.DRAG_FLAG_GLOBAL);
DragShadowBuilder
当触发一个Drag
过程时,系统会产生一个image,用来指示用户手指的移动。这个image叫做drag shadow。为了生成这个image,我们需要自己实现一个DragShadowBuilder
。DragShadowBuilder
的构造方法传入了一个View类型的参数,用来表示发起Drag
的View。
在DragShadowBuilder
里,我们需要实现两个方法:
onProvideShadowMetrics(Point outShadowSize, Point outShadowTouchPoint)
当我们调用startDragAndDrop()
方法后,系统会立即调用onProvideShadowMetrics()
方法。这个方法有两个入参,第一个入参outShadowSize
表示drag shadow显示的尺寸,第二个入参outShadowTouchPoint
表示drag shadow移动时,与手指的接触位置。
onDrawShadow(Canvas canvas)
系统调用onProvideShadowMetrics()
方法后,会立即调用onDrawShadow()
方法,通过Canvas
来绘制drag shadow。
在这里,我们设置drag shadow的尺寸与EditText
一样大,设置手指接触点为drag shadow的中心点,示例代码为:
int width = getView().getWidth();
int height = getView().getHeight();
mShadow.setBounds(0, 0, width, height);
outShadowSize.set(width, height);
outShadowTouchPoint.set(width / 2, height / 2);
到目前为止,当我们移动EditText
时,我们可以看到有个阴影随着我们手指移动了!
DragListener
接下来,我们需要处理整个Drag
过程,并且在合适的时候从WebView
接收数据了。为了获取Drag过程的数据和状态,我们可以为接收该Drag
的目标控件设置一个OnDragListener
,实现其中的onDrag()
方法。onDrag()
方法的定义是这样的:
public boolean onDrag(View view, DragEvent dragEvent) {}
其中,第一个参数view
表示设置了Drag监听器的目标控件,第二个参数dragEvent
表示Drag过程中系统发送的一系列事件。
在Drag过程中,我们可以在DragListener
里收到如下一系列事件:
ACTION_DRAG_STARTED
:表示一个Drag过程的开始。这里我们可以做一些初始化的工作,比如,可以判断此次Drag包含的数据的类型,如果目标控件支持处理该种类型数据,将目标控件加亮,以给用户视觉上的提示。ACTION_DRAG_ENTERED
:表示该过程的drag shadow进入到了目标控件的边界。ACTION_DRAG_EXITED
:表示该过程的drag shadow离开目标控件的边界。ACTION_DROP
:表示用户在目标控件上释放了此次drag过程。我们可以在这个事件发生时获取到此次传递的数据,并且做相应的处理。ACTION_DRAG_ENDED
:表示一次drag过程的结束。
这里需要注意的是,onDrag()
方法返回了一个boolean
类型的返回值,该返回值指定了我们是否可以继续收到当前drag过程的事件。如果返回true
,说明我们对此次drag比较感兴趣,系统还会将后续一系列的drag事件传递给我们的DragListener
;如果返回false
,则我们不会再收到此次drag后续的任何事件。
在MultiWindowGiraffe中,我做了如下处理:
在drag过程中,支持当前数据类型的目标控件置为蓝色;
如果drag shadow移动到了某个控件边界内,如果这个控件支持该数据类型,则将该控件置为绿色。
当drag完成时,在WebView内加载URL。
这里可以参考GiraffeDragEventListener的实现。
至此,我们就可以实现我之前说过的,在Multi-Window
模式下跨窗口传递数据了!!!来看一个效果图吧:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。