开始的开始

自从我的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:MainActivityWebActivity。当处于分屏模式下,我们期待从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了。效果如下:

clipboard.png

下面,我们期待可以用手指将上面这个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数据的对象。暂时不用。

  • flagsDrag过程的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,我们需要自己实现一个DragShadowBuilderDragShadowBuilder的构造方法传入了一个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模式下跨窗口传递数据了!!!来看一个效果图吧:

clipboard.png

参考


Anchorer
431 声望84 粉丝

Show what you're made of.