Android N Multi-Window Mode Support

来源:互联网 发布:男装淘宝店铺运营外包 编辑:程序博客网 时间:2024/04/29 16:53

1. Multi-Window Mode

  1. 如何进入分屏模式?

    • 长按Overview,App进入Split Screen Mode

    • 单击Overview,点击App标题栏上的吕形按钮,进入Split Screen Mode

    • 单击Overview,长按App标题栏拖入屏幕上的高亮区域,然后进入Freeform Screen Mode

  2. 如何配置App支持分屏模式?

    • 项目首先需要满足的

      targetSdkVersion >= Android N Preview

    • 项目AndroidManifest.xml中需要给MainActivity配置:

      android:resizeableActivity = true

      这个属性值在Android N的手机系统默认true,也就是默认支持分屏,前提是满足条件1。

  3. 名词:

    • Full Screen Mode:全屏幕模式

    • Split Screen Mode: 上下屏幕模式

    • Freeform Screen Mode: 自由尺寸屏幕模式

2. 分屏模式下被禁用的特性

  • 分屏模式下无法隐藏系统的状态栏
  • 分屏模式下无法根据屏幕方向旋转App

3. 分屏模式下Activity的生命周期和回调方法

  1. 正常情况下,切换分屏模式的生命周期

    • 长按overview进入分屏模式的Activity生命周期

      MainActivity: onMultiWindowModeChanged
      MainActivity: isInMultiWindowMode:true
      MainActivity: onPause
      MainActivity: onSaveInstanceState
      MainActivity: onStop
      MainActivity: onDestory
      MainActivity: onCreate
      MainActivity: onStart
      MainActivity: onRestoreInstanceState
      MainActivity: onResume
      //注意这里,这里是因为切换到Split Mode的时候,Activity会先立即失去焦点,这点很关键
      MainActivity: onPause

    • 退出分屏模式

      退出分屏模式时,基于分屏模式下Activity的状态,不同的状态生命周期回调有差异

      如果Activity处于 onPause状态,退出分配模式:

      MainActivity: onSaveInstanceState
      MainActivity: onStop
      MainActivity: onDestory
      MainActivity: onCreate
      MainActivity: onStart
      MainActivity: onRestoreInstanceSate
      MainActivity: onResume
      MainActivity: onPause
      MainActivity: onMultiWindowModeChanged
      MainActivity: isInMultiWindowMode:false
      MainActivity: onResume //这个Log不属于退出分屏流程的回调

      如果Activity处于onResume状态,退出分屏模式:

      MainActivity: onPause
      MainActivity: onSaveInstanceState
      MainActivity: onStop
      MainActivity: onDestroy
      MainActivity: onCreate
      MainActivity: onStart
      MainActivity: onRestoreInstanceState
      MainActivity: onResume
      MainActivity: D/MN: onMultiWindowModeChanged
      MainActivity: isInMultiWindowMode= false

      上面的Log输出说明了,如果退出分屏模式之前为onPause状态,则退出之后Activty也必须切换到onPause状态,然后调用的onResume不属于退出分屏的回调流程,属于Activity展示在前台进程获取焦点时的回调。反之,如果退出之前是onResume状态,则退出之后也必须是onResume,则不必再onResume

      总结就是:分屏模式下做一些操作,譬如退出分屏/改变分屏模式的尺寸,Activty在操作前的状态和操作后的状态要保持一致。至于Activity被放置到前台进程时触发的Activty生命周期回调方法,不属于分屏模式下操作的回调。

    • 分屏模式下,从一个Activity切换到和它同处于多窗口的另外一个Activity

      MainActivity: onPause

      SecondActivity: onResume

    • 分屏模式下,改变窗口的尺寸,也需要判断Activty的上一个状态 onPause/onResume

      操作之前处于onPause状态:

      MainActivity:D/MN: onSaveInstanceState
      MainActivity:onStop
      MainActivity:onDestroy
      MainActivity:onCreate
      MainActivity:onStart
      MainActivity:onRestoreInstanceState
      MainActivity:onResume
      MainActivity:onPause

      操作之前处于onResume状态:

      MainActivity:onPause
      MainActivity:onSaveInstanceState
      MainActivity:onStop
      MainActivity:onDestroy
      MainActivity: onCreate
      MainActivity:onStart
      MainActivity:onRestoreInstanceState
      MainActivity:onResume

    由上面的情况可以得知,当切换到多窗口模式或者改变多窗口模式下Activity 的尺寸时,Activity会被销毁然后重新加载。

  2. 特殊情况下,切换分屏模式的生命周期

    为了解决上述的Activity销毁重建的问题,特做了一下配置:

    1. 在AndroidManifest.xml文件中,给Activity加上属性配置如下:

      android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize"

      • 在Activity中复写方法 onConfigurationChanged (不是必须,根据自身的情况来判断是否需要复写)

    在这种情况下由Full ScreenMode切换为Split Screen Mode,其生命周期:

    MainActivity:onPause
    MainActivity:onSaveInstanceState
    MainActivity:onStop
    MainActivity:onConfigurationChanged
    MainActivity:onMultiWindowModeChanged
    MainActivity:isInMultiWindowMode= true
    MainActivity:onRestart
    MainActivity:onStart
    MainActivity:onResume
    MainActivity:onPause

    但是上面的配置,由Full Screen Mode/Split Screen Mode切换为FreeForm Screen Mode并不适用:

    Split Screen Mode —— >FreeForm Screen Mode

    MainActivity:onPause
    MainActivity:onSaveInstanceState
    MainActivity:onStop
    MainActivity:onDestroy
    MainActivity:onCreate
    MainActivity:onStart
    MainActivity:onRestoreInstanceState
    MainActivity:onResume

    Full Screen Mode —— >FreeForm Screen Mode

    MainActivity:onPause
    MainActivity:onSaveInstanceState
    MainActivity:onStop
    MainActivity:onMultiWindowModeChanged
    MainActivity:isInMultiWindowMode= true
    MainActivity:onDestroy
    MainActivity:onCreate
    MainActivity:onStart
    MainActivity:onRestoreInstanceState
    MainActivity:onResume

    基于上面的配置,当处于Split/FreeForm Mode时,如果改变Activity的尺寸:

    MainActivity:onConfigurationChanged

    除此之外,FreeForm Mode切换为 Full Screen Mode,Activity也会销毁重建:

    MainActivity:onPause
    MainActivity:onSaveInstanceState
    MainActivity:onStop
    MainActivity:onDestroy
    MainActivity: onCreate
    MainActivity:onStart
    MainActivity:onRestoreInstanceState
    MainActivity:onResume
    MainActivity:onMultiWindowModeChanged
    MainActivity:isInMultiWindowMode = false

    当Activity A处于Split Screen Mode时,startActivityForResult Activity SECOND,然后切换为FreeForm Screen Mode,最后Activity SECOND Finish 回到Activity A:

    =========startActivityForResult Activity SECOND=====
    MN - A: onPause
    MN - SECOND: onCreate
    MN - SECOND: onStart
    MN - SECOND: onResume
    MN - A: onSaveInstanceState
    MN - A: onStop
    ============Split Mode —> FreeForm Mode =======
    MN-SECOND: onPause
    MN-SECOND: onSaveInstanceState
    MN-SECOND: onStop
    MN-SECOND: onDestroy
    MN-SECOND: onCreate
    MN-SECOND: onStart
    MN-SECOND: onRestoreInstanceState
    MN-SECOND: onResume
    =========B Finish return A ========
    MN-SECOND: onPause
    MN - A: onDestroy
    MN - A: onCreate
    MN - A: onStart
    MN - A: onRestoreInstanceState
    MN - A: onActivityResult
    MN - A: onResume
    MN-SECOND: onStop
    MN-SECOND: onDestroy

  3. 监测多窗口状态的回调方法

    前提是Android N SDK

    1. boolean Activity.isMultiWindowMode()

      判断Activity是否处于Multi-Window(Split / FreeForm Screen Mode)

    2. void Activity.onMultiWindowModeChanged(boolean isInMultiWindowMode)

      Full Scree Mode <——> window-multi 时触发调用

4. 代码中如何判断App处于哪种模式?

首先在特殊情况下,Full Screen Mode 切换到Split Screen Mode不会销毁重建Activity,而切换到FreeForm Screen Mode会销毁重建Activity。
那么,判断multi-window mode 的条件就是:

1.isMultiWindowMode方法返回值为false,则处于Full Screen Mode

2.isMultiWindowMode方法返回值为true,并且调用了onRestoreInstanceState方法,则处于FreeForm Screen Mode,否则处于Split Screen Mode

5. Layout attributes

在Android N 中我们可以向AndroidManifest.xml 中给activity添加 layout节点,并且设置一些属性,通过这些属性来设置分屏模式的一些行为,如最小尺寸等。

android:defaultWidth
android:defaultHeight
这2个属性是freeform模式下默认的宽度和高度

android:gravity
这个是freeform模式下在手机窗口上默认的Gravity

android:minWidth
android:minHeight
这2个属性freeform模式下最小宽度和高度。

下面是一个示例:

<activity android:name=".SecondActivity">        <layout            android:defaultHeight="500dp"            android:defaultWidth="600dp"            android:gravity="top|end"            android:minHeight="450dp"            android:minWidth="300dp"/></activity>

6. Luanch Activity with a defined bounds on Screen

In free-form mode, this activity is to be launched within a defined bounds on screen.
使用这个配置启动的Activity,在由非FreeForm切换为FreeForm模式,则以定义的bounds在屏幕上显示Activity的位置

// Define the bounds in which the Activity will be launched into.Rect bounds = new Rect(500, 300, 100, 0);// Set the bounds as an activity option.ActivityOptions options = ActivityOptions.makeBasic();options.setLaunchBounds(bounds);// Start the LaunchBoundsActivity with the specified optionsIntent intent = new Intent(this, LaunchBoundsActivity.class);startActivity(intent, options.toBundle());

7. 测试APP和Android N Multi-window 适配思路

保证APP可以顺利进入/退出分屏模式,且改变APP的尺寸时,UI依然可以非常顺滑,以及在分屏模式下,仍然可以保持性能的稳定性,不会Crash也不会OOM:

  1. 长按Overview之后,确保App能够进入Split Screen Mode分屏模式,且改变尺寸后仍然能正常工作。

  2. 长按Overview,拖动App标题栏进入FreeForm Screen Mode分屏模式,且改变尺寸后仍然能正常工作。

  3. 分屏模式下,在短时间内、多次、迅速的改变APP的尺寸,确保APP没有崩溃,且没有发生内存泄漏,且UI的刷新没有花费太多的时间。

进一步优化分屏模式:

  1. 减少不可滑动的页面和控件
    在分屏过程中,屏幕的高度只有原来的一半,如果有太多的控件不响应滑动事件,那么用户无法上下滑动页面,甚至无法进行下一步操作。
    这类页面属于最常见的Splash screen、登录注册页、弹窗等。

  2. 尽量使用相对位置,以兼容分屏模式下多种窗口尺寸

  3. 尺寸变化时的处理,比如PopUpWindow自定义键盘

8. Talk is cheap , let us see case!

  1. Activity中存在Fragment使用注意

    在分屏模式下,如果Activity被销毁重建,FragmentManager中存在的Fragments会被自动保存,当再次show/ hide 时,需要注意操作的Fragment是否为同一个Fragment,即是否在FragmentManager中存在同一个Fragment,否则show/hide将失效。

    举个栗子:

    代码段一:初始化Fragment

    fragmentArray = new Fragment[] { new xxxFragment(), ...};ArrayList<Fragment> fragmentList = (ArrayList<Fragment>) mFragmentManager.getFragments();if (fragmentList == null || fragmentList.size() == 0) {    ...    transaction.add(R.id.fl_info, fragmentArray[i], fragmentTagArray[i]);    if (i == mCurrentIndex) {        transaction.show(fragmentArray[i]);    } else {        transaction.hide(fragmentArray[i]);    }}

    代码段二:show/hide

    Fragment fragment = fragmentArray[index];if (fragment != null) {    transaction.show(fragmentArray[i]);} else {    ...}...if (fragment != null) {    transaction.hide(fragmentArray[i]);} else {    ...}

    上面的代码中,当Activity销毁重建之后,fragmentList 不为null,无法添加fragmentArray[i],然后show/hide操作时无效。

    解决办法:

    重写Activity 的 onSaveInstanceState方法,并且不执行super.onSaveInstanceState(outState);

    @Overrideprotected void onSaveInstanceState(Bundle outState) {    //super.onSaveInstanceState(outState);}

    当ViewPager配合Fragment使用时,也需要注意使用,否则会产生切换ViewPager空白。

    根本原因就是操作的Fragment和FragmentManager中存在的不是同一个。

  2. Toast 和 Dialog的使用注意

    首先我们需要了解什么是Window,Window是一个抽象类,它的具体实现是PhoneWindow,其实现过程是在WindowManagerService中。

    Window不能够直接访问,需要借助WindowManager。

    在Android中,所有的视图都是通过Window来呈现,不管是Activity、Dialog还是Toast,他们的视图实际上都是附加在window上的,所以Window是View的实际管理者。

    单击事件由Window传给DecorView,然后传递给我们的View。

    Activity设置视图setContentView也是由Window完成的
    WindowManager.addView(mView);

    WindowManage.LayoutParamas有flags和type参数,其中Type参数表示window的类型,有3种类型,分别是应用window、子window、系统window

    应用window对应一个Activity
    子window不能单独存在,需要附属在特定的父Window之中,比如Dialog
    系统window是需要生命权限才能创建的window,比如Toast和系统状态栏

    Window是分层的,每个window都有对应的z-ordered,层级大的会覆盖在层级小的window上面,在3类window中,应用window层级范围是1-99,子window层级是1000-1999,系统window层级是2000-2999,这些层级范围对应着windowManager.LayoutParamas的Type参数。

    Window是一个抽象的概念,每一个window对应一个View,window和view通过ViewRootImpl联系,所以View是window存在的体现。

    了解了这些,我们来看Dialog和Toast的window创建过程:

    Dialog的window创建过程

    创建window,通过PolicyManager的makeNewWindow方法完成
    初始化DecorView并将Dialog的视图添加到DecorView中
    WindowManager将DecorView添加到window中并显示
    普通的Dialog有特殊之处,那就是必须采用Activity的Context,一般来说Dialog的window依附于Activity的window

    Toast的window创建过程:

    首先Toast也是基于window实现的,但是和Dialog不同,具体的就不详述了
    Toast属于系统window,属于整个屏幕范围的window

    那么,当手机分屏时,如果使用了Toast,则Toast的会保持原位置不变。

    所以为了适配分屏模式,不建议使用Toast,可以使用Dialog代替

  3. PopUpWindow自定义键盘在尺寸变化时如何适配

    在分屏模式下,APP的尺寸产生了变化,PopUpWindow自定义键盘会遮盖掉整个EditText,这显然不是我们想要看到的,我们想要的是系统键盘那样将EditText放在键盘的上方。

    思路很简单,在显示PopUpWindow之前,计算出EditText的bottom应该所在的准确位置,然后得到原位置和理想位置的distance,然后整体布局Move distance,当键盘隐藏,自动回到原位置。

    首先我们需要了解一些东西:

    如何获取屏幕尺寸?

    如何准确获取EditText原位置?

    如何计算Distance,然后Move布局到理想效果?

    获取屏幕尺寸:

    DisplayMetrics dm = null;activity.getWindowManager().getDefaultDisplay().getMetrics(dm);int screenHeight = dm.heightPixels;

    准确获取EditText原位置

    int editTextBottom = 0;Rect rect = new Rect();//获取view在视图屏幕范围内的坐标,如果view被遮挡,则返回false, rect(0,0,0,0)boolean globalVisibleRect = editText.getGlobalVisibleRect(rect);if (globalVisibleRect){    editTextBottom = rect.bottom;}

    计算Distance并Move blockLayout:

    if(editTextBottom!=0) {  int scrollY = blockLayout.getScrollY();  int popUpWindowTop = screenHeight - popUpWindowHeight;  distance = editTextBottom - (popUpWindowTop);  if (scrollY + distance < 0) {        distance = -scrollY;//防止多次向上移动 x * distance之后,无法复位  }  if (blockLayout instanceof AutoPopLinearLayout) {      ((AutoPopLinearLayout) blockLayout).startScroll(scrollY,distance,500);  }}

    blockLayout是EditText外层布局LinearLayout:

    public class AutoPopLinearLayout extends LinearLayout {    private final Context context;    private final Scroller mScroller;    private boolean isMove;    public AutoPopLinearLayout(Context context, AttributeSet attrs) {        super(context, attrs);        this.context = context;        DecelerateInterpolator interpolator = new DecelerateInterpolator();        mScroller = new Scroller(context,interpolator);    }    /**     * mScroller是一个封装位置和速度等信息的变量.     * startScroll函数只是对它的一些成员变量做一些记录和计算.     * 这个函数的结果就是导致mScroller.computeScrollOffset()返回true     * 以及触发computeScroll方法的调用     *     * 布局坐标体系是以左上方为(0,0)     *     * @param startY Y方向偏移量,作为开始位置     * @param dy 移动的Y方向上的距离,dy大于0,则布局往上移动,反之向下     * @param duration     */    public void startScroll(int startY, int dy, int duration) {        isMove = true;        mScroller.startScroll(0,startY,0,dy,duration);        invalidate();    }    @Override    public void computeScroll() {        /*         * 如果mScroller没有调用startScroll,这里将返回false。         * scrollTo方法依据封装在mScroller中的位置信息对blockLayout中的childView进行移动         */        if (mScroller.computeScrollOffset()) {            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());            postInvalidate();            isMove = true;        } else {            isMove = false;        }        super.computeScroll();    }    public boolean isMove() {        return isMove;    }}

    这里主要需要了解的是: Scroller的概念

    Scroller类是为了实现View平滑滚动的一个Helper类,通常是咋自定义View的时候使用。

    如上代码中,mScroller记录和计算View滚动的位置,调用startScroll方法驱动,再重写View的computeScroll(),完成实际的滚动,实际的滚动通过scrollTo方法完成。

    这里需要知道的一些API:

    View.getGlobalVisibleRect(Rect rect)

    顾名思义就是获取view的全局可视左上右下坐标,坐标原点时左上角(0,0),右下方向为正方向

    View.getScrollY()

    return the edge of top of the display part of you view返回view的可视部分的top边缘位置坐标

    scrollTo(x,y)

    scrolled to one postion(x,y)

    Scroller.getCurrX()/getCurrY()

    return current new offset X/Y in the scroll返回新的偏移位置 X/Y
  4. 如果某个Activity不需要支持分屏如何处理

    使用下面代码启动Activity:

    Intent intent = new Intent(this, XXXActivity.class);intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT);startActivity(intent);

    并且Activity需要设置属性:

    <activity    android:resizeableActivity="false"    android:excludeFromRecents="true"    android:name=".ThirdActivity"></activity>

    这样子在新的任务栈中开启Activity,就会以全屏方式打开,并且不支持分屏。其中android:excludeFromRecents="true"是为finish掉Activity之后,清除Recents中的Activity。

  5. 分屏模式下Activity销毁重建时的数据缓存,Temp数据缓存

    主要是Activity的重建缓存机制,涉及到2个方法:

    @Overrideprotected void onSaveInstanceState(Bundle outState) {    super.onSaveInstanceState(outState);    Log.d("N","onSaveInstanceState");}@Override    protected void onRestoreInstanceState(Bundle savedInstanceState) {    super.onRestoreInstanceState(savedInstanceState);    Log.d("N","onRestoreInstanceState");}
  6. 如何适配不可滑动的页面,以及解决滑动时产生的问题

    分屏模式下由于尺寸的变化,一些不可滑动的页面只能显示部分内容,所以针对这些页面需要在布局外层嵌套ScrollView,并且设置android:fillViewport="true"

    <ScrollView    android:fillViewport="true"    android:layout_width="match_parent"    android:layout_height="match_parent">    <LinearLayout        android:orientation="vertical"        android:gravity="center_horizontal"        android:layout_width="match_parent"        android:layout_height="match_parent">               ...    </LinearLayout></ScrollView>

    由于ScrollView嵌套之后,导致LinearLayout 的 match_parent不生效,会以wrap_content来计算,在大屏手机如三星手机上显示不全,所以需要设置 android:fillViewport=”true”属性。

    值得注意的是,如果ScrollView嵌套ViewPager的时候,ViewPager无法正确显示高度的,导致内容显示不全,以及ViewPager左右滑动冲突的问题。

    这个时候就需要自定义ViewPager,栗如:

    public class ResetViewPager extends ViewPager {    private int lastX = -1;    private int lastY = -1;    public ResetViewPager(Context context) {        super(context);    }    public ResetViewPager(Context context, AttributeSet attrs) {        super(context, attrs);    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        heightMeasureSpec = resetHeightMeasureSpec(widthMeasureSpec);        super.onMeasure(widthMeasureSpec, heightMeasureSpec);    }    /**     * 重新计算 heightMeasureSpec     * @param widthMeasureSpec     * @return     */    private int resetHeightMeasureSpec(int widthMeasureSpec) {        int heightMeasureSpec;        int height = 0;        for (int i = 0; i < getChildCount(); i++) {            View childAt = getChildAt(i);            childAt.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));            int h = childAt.getMeasuredHeight();            if (h > height) {                height = h;            }        }        heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);        return heightMeasureSpec;    }    @Override    public boolean dispatchTouchEvent(MotionEvent ev) {        int rawX = (int) ev.getRawX();        int rawY = (int) ev.getRawY();        int dealtX = 0;        int dealtY = 0;        switch (ev.getAction()) {            case MotionEvent.ACTION_DOWN:                dealtX = 0;                dealtY = 0;                //保证子ViewPager能收到ACTION_MOVE事件                getParent().requestDisallowInterceptTouchEvent(true);                break;            case MotionEvent.ACTION_MOVE:                dealtX += Math.abs(rawX - lastX);                dealtY += Math.abs(rawY - lastY);                //这里的拦截判断依据是左右滑动                if(dealtX >= dealtY){                    getParent().requestDisallowInterceptTouchEvent(true);                }else{                    getParent().requestDisallowInterceptTouchEvent(false);                }                lastX = rawX;                lastY = rawY;                break;            case MotionEvent.ACTION_CANCEL:                break;            case MotionEvent.ACTION_UP:                break;        }        return super.dispatchTouchEvent(ev);    }}
  7. ListView在分屏模式时的使用注意

    示栗:

    代码段一

    @Overridepublic int getItemViewType(int position) {    ...    return (rpipttyp.equals("00") || rpipttyp.equals("13"))                         ? R.layout.item_lv_select_condition2 : R.layout.item_lv_select_condition;}@Overridepublic int getViewTypeCount() {    return 2;}

    代码段二

    int ViewType = getItemViewType(position);switch (ViewType) {    case R.layout.item_lv_select_condition2:            ...    break;    case R.layout.item_lv_select_condition:         ...    break;}

    上面的代码是根据 position返回 Item的 Type,以及在 getView() 的时候判断Type

    但是上面的代码写法在分屏模式下会产生异常:

    Exception: ArrayIndexOfBoundsException

    原因是:

    The ListView item view type you are returning from getItemViewType() is < getViewTypeCount()

    也就是说:ListView使用Adapater时,getViewTypeCount() 方法和 getItemViewType() 方法返回值之间有一定的关系。

    如果 getViewTypeCount 返回值为2,那么 getItemViewType(position) 方法的返回值应该为0,1,不能超过1,否则会出现 ArrayIndexOfBoundsException

    栗如:

    @Overridepublic int getItemViewType(int position) {    ...    return (rpipttyp.equals("00") || rpipttyp.equals("13")) ? 0: 1;}

9. 附录

  • Documents - Android developer multi-window doc
  • Demo - Android MultiWindowPlayground-master
原创粉丝点击