Android Nougat - 有点好玩的Multi-Window

开始的开始

自从个人Nexus 6加入了Android beta计划以来,我便在很早的时候就体验上了Android Nougat的一些新特性,天然也体验到了比较重要的Multi-Window新特性。不过,在当前Android Nougat普及度特别低(尤为是个人Nexus 6尚未收到官方ota)的状况下,Multi-Window的体验也大打了很多折扣。html

根据民间的说法,Multi-Window能够同时在屏幕上打开两个应用,你能够在用其中一个App的同时看到另外一个App的内容。想象一下,你正在玩游戏或者看电影,这时你可能同时正在和朋友聊天,这在以往,你可能须要整屏切来切去,如今,你能够在同一个屏幕上完成这两件事情,并且互不耽误。再想象一下,你正在和朋友探讨知识,忽然遇到了一个你讲不清楚的原理,这时你开了一个分屏打开了Google,这边Google一下答案,复制一下,而后立马在那边继续大家的探讨,这大大加快了你获取知识的效率,同时也为装逼提供了可能。java

通过初步的探索,我发现Multi-Window的分屏并非以App为基本单位,确切地说,是以Activity做为基本单位。这就意味着,同一个App的不一样的Activity,也能够共享屏幕。再精确地说,咱们能够开发出一款App,这个App能够打开两个窗口分享整个屏幕,每一个窗口加载一个不一样的Activity。想象一下,若是有一个邮件App,能够左边窗口查看收件箱,右边窗口写新邮件,这可能会方便不少。再想象一下,若是有一个象棋游戏,你能够左边窗口扮演玩家A,右边窗口扮演玩家B,本身和本身下棋。哦剩下的脑洞交给大家了。android

其实当我首次看到Multi-Window的介绍时,我不认为这个特性会带来多少方便,由于我感受手机屏幕原本就比较小,再分屏就有点施展不开了;这个特性相对地对于平板更好一些。直到我看到Android里面还有一个叫作Drag and Drop的东西,这让我感受Multi-Window能多多少少施展一些做用了。git

Drag and Drop让咱们能够把数据从一个View拖到另外一个View。比方说,咱们有两个EditText,其中在第一个EditText输入了某项内容,而后咱们用手指从第一个EditText拖动到第二个EditText,而后第二个EditText就自动填充了第一个EditText的内容,是否是很好玩?这里传递的数据能够是任何咱们须要的数据,而View是任何咱们能够操做的控件,这就为咱们的交互设计提供了更多的想象空间。更重要的,Drag and Drop很好地支持了Multi-Window,这为跨窗口数据分享提供了可能。github

这里我实现了这样一个App:这个App启动了两个Activity进入分屏模式,第一个Activity有一个输入URL的EditText,第二个Activity有一个WebView。咱们在第一个Activity中输入了URL以后,用手从EditText跨窗口拖动到第二个Activity的WebView上,就直接在WebView中打开咱们填写的URL。canvas

下面咱们来一步一步来探索一下这些具体是怎么完成的。这个工程我放在了Github-MultiWindowGiraffe,以供参考。app

Multi-Window

在Android Nougat上进入Multi-Window模式的方法能够参照Multi-Window Support上的说明,这里只从开发层面加以描述。ide

当App进入Multi-Window模式时,系统会向Activity发送一个configuration change的通知,其对Activity生命周期的影响和屏幕旋转是同样的。当处于Multi-Window模式时,咱们能够看到两个Activity分享屏幕,其中用户正在操做的Activity处于Active状态,另外一个处于Paused状态。用户的操做从其中一个Activity转到另外一个Activity时,会调用原来Activity的onPause()方法,同时会触发新操做的Activity的onResume()方法。ui

所以,这里出现了一个处于Paused状态但同时又对用户可见的Activity。咱们知道,当Activity对用户不可见时,会调用生命周期的onPause()onStop()方法,咱们能够在这两个方法里面作一些相似中止视频播放的操做。可是在这里,当分屏的两个Activity之间切换时,只会触发onPause()方法,若是咱们想要用户在另外一个窗口操做的时候视频扔继续播放,就只能将中止播放的动做放在onStop()方法里,不能放到onPause()方法里了。this

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

参考

相关文章
相关标签/搜索