Android 三种动画详解

Android 三种动画详解

1 背景

不能只分析源码呀,分析的同时也要整理归纳基础知识,刚好有人微博私信让全面说说Android的动画,所以今天来一发Android应用的各种Animation大集合。英文厉害的请直接移步参考Android Developer。

Android系统提供了很多丰富的API去实现UI的2D与3D动画,*主要的划分可以分为如下几类:

  • View Animation: 视图动画在古老的Android版本系统中就已经提供了,只能被用来设置View的动画。
  • Drawable Animation: 这种动画(也叫Frame动画、帧动画)其实可以划分到视图动画的类别,专门用来一个一个的显示Drawable的resources,就像放幻灯片一样。
  • Property Animation: 属性动画只对Android 3.0(API 11)以上版本的Android系统才有效,这种动画可以设置给任何Object,包括那些还没有渲染到屏幕上的对象。这种动画是可扩展的,可以让你自定义任何类型和属性的动画。

可以看见,当前应用程序开发涉及的主要动画也就这三大类,我们接下来以类别为基础来慢慢展开说明。

 

2 View Animation(视图动画)使用详解

2-1 视图动画概述

视图动画,也叫Tween(补间)动画可以在一个视图容器内执行一系列简单变换(位置、大小、旋转、透明度)。譬如,如果你有一个TextView对象,您可以移动、旋转、缩放、透明度设置其文本,当然,如果它有一个背景图像,背景图像会随着文本变化。

补间动画通过XML或Android代码定义,建议使用XML文件定义,因为它更具可读性、可重用性。

如下是视图动画相关的类继承关系:

这里写图片描述

java类名 xml关键字 描述信息
AlphaAnimation <alpha> 放置在res/anim/目录下 渐变透明度动画效果
RotateAnimation <rotate> 放置在res/anim/目录下 画面转移旋转动画效果
ScaleAnimation <scale> 放置在res/anim/目录下 渐变尺寸伸缩动画效果
TranslateAnimation <translate> 放置在res/anim/目录下 画面转换位置移动动画效果
AnimationSet <set> 放置在res/anim/目录下 一个持有其它动画元素alpha、scale、translate、rotate或者其它set元素的容器

通过上图和上表可以直观的看出来补间动画的关系及种类了吧,接下来我们就详细一个一个的介绍一下各种补间动画。

2-2 视图动画详细说明

可以看出来Animation抽象类是所有补间动画类的基类,所以基类会提供一些通用的动画属性方法,如下我们就来详细看看这些属性,关于这些属性详细官方解释FQ点击我或者FQ点击我。

2-2-1 Animation属性详解

xml属性 java方法 解释
android:detachWallpaper setDetachWallpaper(boolean) 是否在壁纸上运行
android:duration setDuration(long) 动画持续时间,毫秒为单位
android:fillAfter setFillAfter(boolean) 控件动画结束时是否保持动画*后的状态
android:fillBefore setFillBefore(boolean) 控件动画结束时是否还原到开始动画前的状态
android:fillEnabled setFillEnabled(boolean) 与android:fillBefore效果相同
android:interpolator setInterpolator(Interpolator) 设定插值器(指定的动画效果,譬如回弹等)
android:repeatCount setRepeatCount(int) 重复次数
android:repeatMode setRepeatMode(int) 重复类型有两个值,reverse表示倒序回放,restart表示从头播放
android:startOffset setStartOffset(long) 调用start函数之后等待开始运行的时间,单位为毫秒
android:zAdjustment setZAdjustment(int) 表示被设置动画的内容运行时在Z轴上的位置(top/bottom/normal),默认为normal

也就是说,无论我们补间动画的哪一种都已经具备了这种属性,也都可以设置使用这些属性中的一个或多个。

那接下来我们就看看每种补间动画特有的一些属性说明吧。

2-2-2 Alpha属性详解

xml属性 java方法 解释
android:fromAlpha AlphaAnimation(float fromAlpha, …) 动画开始的透明度(0.0到1.0,0.0是全透明,1.0是不透明)
android:toAlpha AlphaAnimation(…, float toAlpha) 动画结束的透明度,同上

2-2-3 Rotate属性详解

xml属性 java方法 解释
android:fromDegrees RotateAnimation(float fromDegrees, …) 旋转开始角度,正代表顺时针度数,负代表逆时针度数
android:toDegrees RotateAnimation(…, float toDegrees, …) 旋转结束角度,正代表顺时针度数,负代表逆时针度数
android:pivotX RotateAnimation(…, float pivotX, …) 缩放起点X坐标(数值、百分数、百分数p,譬如50表示以当前View左上角坐标加50px为初始点、50%表示以当前View的左上角加上当前View宽高的50%做为初始点、50%p表示以当前View的左上角加上父控件宽高的50%做为初始点)
android:pivotY RotateAnimation(…, float pivotY) 缩放起点Y坐标,同上规律

2-2-4 Scale属性详解

xml属性 java方法 解释
android:fromXScale ScaleAnimation(float fromX, …) 初始X轴缩放比例,1.0表示无变化
android:toXScale ScaleAnimation(…, float toX, …) 结束X轴缩放比例
android:fromYScale ScaleAnimation(…, float fromY, …) 初始Y轴缩放比例
android:toYScale ScaleAnimation(…, float toY, …) 结束Y轴缩放比例
android:pivotX ScaleAnimation(…, float pivotX, …) 缩放起点X轴坐标(数值、百分数、百分数p,譬如50表示以当前View左上角坐标加50px为初始点、50%表示以当前View的左上角加上当前View宽高的50%做为初始点、50%p表示以当前View的左上角加上父控件宽高的50%做为初始点)
android:pivotY ScaleAnimation(…, float pivotY) 缩放起点Y轴坐标,同上规律

2-2-5 Translate属性详解

xml属性 java方法 解释
android:fromXDelta TranslateAnimation(float fromXDelta, …) 起始点X轴坐标(数值、百分数、百分数p,譬如50表示以当前View左上角坐标加50px为初始点、50%表示以当前View的左上角加上当前View宽高的50%做为初始点、50%p表示以当前View的左上角加上父控件宽高的50%做为初始点)
android:fromYDelta TranslateAnimation(…, float fromYDelta, …) 起始点Y轴从标,同上规律
android:toXDelta TranslateAnimation(…, float toXDelta, …) 结束点X轴坐标,同上规律
android:toYDelta TranslateAnimation(…, float toYDelta) 结束点Y轴坐标,同上规律

2-2-6 AnimationSet详解

AnimationSet继承自Animation,是上面四种的组合容器管理类,没有自己特有的属性,他的属性继承自Animation,所以特别注意,当我们对set标签使用Animation的属性时会对该标签下的所有子控件都产生影响。

2-3 视图动画使用方法

通过上面对于动画的属性介绍之后我们来看看在Android中这些动画如何使用(PS:这里直接演示xml方式,至于Java方式太简单了就不说了),如下:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@[package:]anim/interpolator_resource"
    android:shareInterpolator=["true" | "false"] >
    <alpha
        android:fromAlpha="float"
        android:toAlpha="float" />
    <scale
        android:fromXScale="float"
        android:toXScale="float"
        android:fromYScale="float"
        android:toYScale="float"
        android:pivotX="float"
        android:pivotY="float" />
    <translate
        android:fromXDelta="float"
        android:toXDelta="float"
        android:fromYDelta="float"
        android:toYDelta="float" />
    <rotate
        android:fromDegrees="float"
        android:toDegrees="float"
        android:pivotX="float"
        android:pivotY="float" />
    <set>
        ...
    </set>
</set>

 

ImageView spaceshipImage = (ImageView) findViewById(R.id.spaceshipImage);
Animation hyperspaceJumpAnimation = AnimationUtils.loadAnimation(this, R.anim.hyperspace_jump);
spaceshipImage.startAnimation(hyperspaceJumpAnimation);

 

上面就是一个标准的使用我们定义的补间动画的模板。至于补间动画的使用,Animation还有如下一些比较实用的方法介绍:

Animation类的方法 解释
reset() 重置Animation的初始化
cancel() 取消Animation动画
start() 开始Animation动画
setAnimationListener(AnimationListener listener) 给当前Animation设置动画监听
hasStarted() 判断当前Animation是否开始
hasEnded() 判断当前Animation是否结束

既然补间动画只能给View使用,那就来看看View中和动画相关的几个常用方法吧,如下:

View类的常用动画操作方法 解释
startAnimation(Animation animation) 对当前View开始设置的Animation动画
clearAnimation() 取消当View在执行的Animation动画

到此整个Android的补间动画常用详细属性及方法全部介绍完毕,如有特殊的属性需求可以访问Android Developer查阅即可。如下我们就来个综合大演练。

2-4 视图动画注意事项

关于视图动画(补间动画)的例子我就不介绍了,网上简直多的都泛滥了。只是强调在使用补间动画时注意如下一点即可:

特别特别注意:补间动画执行之后并未改变View的真实布局属性值。切记这一点,譬如我们在Activity中有一个 Button在屏幕上方,我们设置了平移动画移动到屏幕下方然后保持动画*后执行状态呆在屏幕下方,这时如果点击屏幕下方动画执行之后的Button是没 有任何反应的,而点击原来屏幕上方没有Button的地方却响应的是点击Button的事件。

2-5 视图动画Interpolator插值器详解

2-5-1 插值器简介

介绍补间动画插值器之前我们先来看一幅图,如下:

这里写图片描述

可以看见其实各种插值器都是实现了Interpolator接口而已,同时可以看见系统提供了许多已经实现OK的插值器,具体如下:

java类 xml id值 描述
AccelerateDecelerateInterpolator @android:anim/accelerate_decelerate_interpolator 动画始末速率较慢,中间加速
AccelerateInterpolator @android:anim/accelerate_interpolator 动画开始速率较慢,之后慢慢加速
AnticipateInterpolator @android:anim/anticipate_interpolator 开始的时候从后向前甩
AnticipateOvershootInterpolator @android:anim/anticipate_overshoot_interpolator 类似上面AnticipateInterpolator
BounceInterpolator @android:anim/bounce_interpolator 动画结束时弹起
CycleInterpolator @android:anim/cycle_interpolator 循环播放速率改变为正弦曲线
DecelerateInterpolator @android:anim/decelerate_interpolator 动画开始快然后慢
LinearInterpolator @android:anim/linear_interpolator 动画匀速改变
OvershootInterpolator @android:anim/overshoot_interpolator 向前弹出一定值之后回到原来位置
PathInterpolator 新增,定义路径坐标后按照路径坐标来跑。

如上就是系统提供的一些插值器,下面我们来看看怎么使用他们。

2-5-2 插值器使用方法

插值器的使用比较简答,如下:

<set android:interpolator="@android:anim/accelerate_interpolator">
    ...
</set>

 

2-5-3 插值器的自定义

有时候你会发现系统提供的插值器不够用,可能就像View一样需要自定义。所以接下来我们来看看插值器的自定义,关于插值器的自定义分为两种实现方式,xml自定义实现(其实就是对现有的插值器的一些属性修改)或者java代码实现方式。如下我们来说说。

先看看XML自定义插值器的步骤:

  • 在res/anim/目录下创建filename.xml文件。
  • 修改你准备自定义的插值器如下:
<?xml version="1.0" encoding="utf-8"?>
<InterpolatorName xmlns:android="http://schemas.android.com/apk/res/android"
    android:attribute_name="value"
    />

 

  • 在你的补间动画文件中引用该文件即可。

可以看见上面第二步修改的是现有插值器的一些属性,但是有些插值器却不具备修改属性,具体如下:

<accelerateDecelerateInterpolator>

无可自定义的attribute。

<accelerateInterpolator>

android:factor 浮点值,加速速率(默认值为1)。

<anticipateInterploator>

android:tension 浮点值,起始点后拉的张力数(默认值为2)。

<anticipateOvershootInterpolator>

android:tension 浮点值,起始点后拉的张力数(默认值为2)。
android:extraTension 浮点值,拉力的倍数(默认值为1.5)。

<bounceInterpolator>

无可自定义的attribute。

<cycleInterplolator>

android:cycles 整形,循环的个数(默认为1)。

<decelerateInterpolator>

android:factor 浮点值,减速的速率(默认为1)。

<linearInterpolator>

无可自定义的attribute。

<overshootInterpolator>

android:tension 浮点值,超出终点后的张力(默认为2)。

再来看看Java自定义插值器的(Java自定义插值器其实是xml自定义的升级,也就是说如果我们修改xml的属性还不能满足需求,那就可以选择通过Java来实现)方式。

可以看见上面所有的Interpolator都实现了Interpolator接口,而Interpolator接口又继承自 TimeInterpolator,TimeInterpolator接口定义了一个float getInterpolation(float input);方法,这个方法是由系统调用的,其中的参数input代表动画的时间,在0和1之间,也就是开始和结束之间。

如下就是一个动画始末速率较慢、中间加速的AccelerateDecelerateInterpolator插值器:

public class AccelerateDecelerateInterpolator extends BaseInterpolator
        implements NativeInterpolatorFactory {
    ......
    public float getInterpolation(float input) {
        return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
    }
    ......
}

 

到此整个补间动画与补间动画的插值器都分析完毕了,接下来看下别的动画。

 

3 Drawable Animation(Drawable动画)使用详解

3-1 Drawable动画概述

Drawable动画其实就是Frame动画(帧动画),它允许你实现像播放幻灯片一样的效果,这种动画的实质其实是Drawable,所以这种动画的XML定义方式文件一般放在res/drawable/目录下。具体关于帧动画的xml使用方式FQ点击我查看,java方式FQ点击我查看。

如下图就是帧动画的源码文件:

这里写图片描述

可以看见实际的真实父类就是Drawable。

3-2 Drawable动画详细说明

我们依旧可以使用xml或者java方式实现帧动画。但是依旧推荐使用xml,具体如下:

<animation-list> 必须是根节点,包含一个或者多个<item>元素,属性有:

  • android:oneshot true代表只执行一次,false循环执行。
  • <item> 类似一帧的动画资源。

<item> animation-list的子项,包含属性如下:

  • android:drawable 一个frame的Drawable资源。
  • android:duration 一个frame显示多长时间。

3-3 Drawable动画实例演示

关于帧动画相对来说比较简单,这里给出一个常规使用框架,如下:

<!-- 注意:rocket.xml文件位于res/drawable/目录下 -->
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot=["true" | "false"] >
    <item
        android:drawable="@[package:]drawable/drawable_resource_name"
        android:duration="integer" />
</animation-list>

 

ImageView rocketImage = (ImageView) findViewById(R.id.rocket_image);
rocketImage.setBackgroundResource(R.drawable.rocket_thrust);

rocketAnimation = (AnimationDrawable) rocketImage.getBackground();
rocketAnimation.start();

 

特别注意,AnimationDrawable的start()方法不能在Activity的onCreate方法中调运,因为AnimationDrawable还未完全附着到window上,所以*好的调运时机是onWindowFocusChanged()方法中。

至此帧动画也就说明完成了。让我们接下来进入Android更牛叉的动画类型。

 

4 Property Animation(属性动画)使用详解

在使用属性动画之前先来看几个常用的View属性成员:

  • translationX,translationY:控制View的位置,值是相对于View容器左上角坐标的偏移。
  • rotationX,rotationY:控制相对于轴心旋转。
  • x,y:控制View在容器中的位置,即左上角坐标加上translationX和translationY的值。
  • alpha:控制View对象的alpha透明度值。

这几个常用的属性相信大家都很熟悉,接下来的属性动画我们就从这里展开。

4-1 属性动画概述

Android 3.0以后引入了属性动画,属性动画可以轻而易举的实现许多View动画做不到的事,上面也看见了,View动画无非也就做那几种事情,别的也搞不定,而 属性动画就可以的,譬如3D旋转一张图片。其实说白了,你记住一点就行,属性动画实现原理就是修改控件的属性值实现的动画。

具体先看下类关系:

/**
 * This is the superclass for classes which provide basic support for animations which can be
 * started, ended, and have <code>AnimatorListeners</code> added to them.
 */
public abstract class Animator implements Cloneable {
    ......
}

 

所有的属性动画的抽象基类就是他。我们看下他的实现子类:
这里写图片描述
其实可以看见,属性动画的实现有7个类(PS,之所以类继承关系列表会出来那么多是因为我下载了所有版本的SDK,你只用关注我红点标注的就行,妹 的,ubuntu下图片处理工具怎么都这么难用),进去粗略分析可以发现,好几个是hide的类,而其他可用的类继承关系又如下:

这里写图片描述

java类名 xml关键字 描述信息
ValueAnimator <animator> 放置在res/animator/目录下 在一个特定的时间里执行一个动画
TimeAnimator 不支持/点我查看原因 时序监听回调工具
ObjectAnimator <objectAnimator> 放置在res/animator/目录下 一个对象的一个属性动画
AnimatorSet <set> 放置在res/animator/目录下 动画集合

所以可以看见,我们平时使用属性动画的重点就在于AnimatorSet、ObjectAnimator、TimeAnimator、ValueAnimator。所以接下来我们就来依次说说如何使用。

4-2 属性动画详细说明

4-2-1 属性动画计算原理

参看Android官方文档,英文原版详情点我查看!

Android属性动画(注意*低兼容版本,不过可以使用开源项目来替代低版本问题)提供了以下属性:

  • Duration:动画的持续时间;
  • TimeInterpolation:定义动画变化速率的接口,所有插值器都必须实现此接口,如线性、非线性插值器;
  • TypeEvaluator:用于定义属性值计算方式的接口,有int、float、color类型,根据属性的起始、结束值和插值一起计算出当前时间的属性值;
  • Animation sets:动画集合,即可以同时对一个对象应用多个动画,这些动画可以同时播放也可以对不同动画设置不同的延迟;
  • Frame refreash delay:多少时间刷新一次,即每隔多少时间计算一次属性值,默认为10ms,*终刷新时间还受系统进程调度与硬件的影响;
  • Repeat Country and behavoir:重复次数与方式,如播放3次、5次、无限循环,可以让此动画一直重复,或播放完时向反向播放;

接下来先来看官方为了解释原理给出的两幅图(其实就是初中物理题,不解释):

这里写图片描述
上面就是一个线性匀速动画,描述了一个Object的X属性运动动画,该对象的X坐标在40ms内从0移动到40,每10ms刷新一次,移动4次,每次移动为40/4=10pixel。
这里写图片描述
上面是一个非匀速动画,描述了一个Object的X属性运动动画,该对象的X坐标在40ms内从0移动到40,每10ms刷新一次,移动4次,但是速率不同,开始和结束的速度要比中间部分慢,即先加速后减速。

接下来我们来详细的看一下,属性动画系统的重要组成部分是如何计算动画值的,下图描述了如上面所示动画的实现作用过程。

这里写图片描述

其中的ValueAnimator是动画的执行类,跟踪了当前动画的执行时间和当前时间下的属性值;ValueAnimator封装了动画的 TimeInterpolator时间插值器和一个TypeEvaluator类型估值,用于设置动画属性的值,就像上面图2非线性动画 里,TimeInterpolator使用了AccelerateDecelerateInterpolator、TypeEvaluator使用了 IntEvaluator。

为了执行一个动画,你需要创建一个ValueAnimator,并且指定目标对象属性的开始、结束值和持续时间。在调用start后,整个动画过程 中, ValueAnimator会根据已经完成的动画时间计算得到一个0到1之间的分数,代表该动画的已完成动画百分比。0表示0%,1表示100%,譬如上 面图一线性匀速动画中总时间 t = 40 ms,t = 10 ms的时候是 0.25。

当ValueAnimator计算完已完成动画分数后,它会调用当前设置的TimeInterpolator,去计算得到一个 interpolated(插值)分数,在计算过程中,已完成动画百分比会被加入到新的插值计算中。如上图2非线性动画中,因为动画的运动是缓慢加速的, 它的插值分数大约是 0.15,小于t = 10ms时的已完成动画分数0.25。而在上图1中,这个插值分数一直和已完成动画分数是相同的。

当插值分数计算完成后,ValueAnimator会根据插值分数调用合适的 TypeEvaluator去计算运动中的属性值。

好了,现在我们来看下代码就明白这段话了,上面图2非线性动画里,TimeInterpolator使用了 AccelerateDecelerateInterpolator、TypeEvaluator使用了IntEvaluator。所以这些类都是标准的 API,我们来看下标准API就能类比自己写了,如下:

首先计算已完成动画时间分数(以10ms为例):t=10ms/40ms=0.25。

接着看如下源码如何实现计算差值分数的:

public class AccelerateDecelerateInterpolator extends BaseInterpolator
        implements NativeInterpolatorFactory {
    public AccelerateDecelerateInterpolator() {
    }
    ......
    //这是我们关注重点,可以发现如下计算公式计算后(input即为时间因子)插值大约为0.15。
    public float getInterpolation(float input) {
        return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
    }
    ......
}

 

其实AccelerateDecelerateInterpolator的基类接口就是TimeInterpolator,如下,他只有getInterpolation方法,也就是上面我们关注的方法。

public interface TimeInterpolator {
    float getInterpolation(float input);
}

 

接着ValueAnimator会根据插值分数调用合适的TypeEvaluator(IntEvaluator)去计算运动中的属性值,如下,因为startValue = 0,所以属性值:0+0.15*(40-0)= 6。

public class IntEvaluator implements TypeEvaluator<Integer> {
    public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
        int startInt = startValue;
        return (int)(startInt + fraction * (endValue - startInt));
    }
}

 

这就是官方给的一个关于属性动画实现的过程及基本原理解释,相信你看到这里是会有些迷糊的,没关系,你先有个大致概念就行,接下来我们会慢慢进入实战,因为Android的属性动画相对于其他动画来说涉及的知识点本来就比较复杂,所以我们慢慢来。

4-2-2 XML方式属性动画

在xml中可直接用的属性动画节点有ValueAnimator、ObjectAnimator、AnimatorSet。如下是官方的一个例子和解释(详情点我):

<set
  android:ordering=["together" | "sequentially"]>

    <objectAnimator
        android:propertyName="string"
        android:duration="int"
        android:valueFrom="float | int | color"
        android:valueTo="float | int | color"
        android:startOffset="int"
        android:repeatCount="int"
        android:repeatMode=["repeat" | "reverse"]
        android:valueType=["intType" | "floatType"]/>

    <animator
        android:duration="int"
        android:valueFrom="float | int | color"
        android:valueTo="float | int | color"
        android:startOffset="int"
        android:repeatCount="int"
        android:repeatMode=["repeat" | "reverse"]
        android:valueType=["intType" | "floatType"]/>

    <set>
        ...
    </set>
</set>

 

<set>属性解释:

xml属性 解释
android:ordering 控制子动画启动方式是先后有序的还是同时进行。sequentially:动画按照先后顺序;together(默认):动画同时启动;

<objectAnimator>属性解释:

xml属性 解释
android:propertyName String类型,必须要设置的节点属性,代表要执行动画的属性(通过名字引用),辟如你可以指定了一个View的”alpha” 或者 “backgroundColor” ,这个objectAnimator元素没有对外说明target属性,所以你不能在XML中设置执行这个动画,必须通过调用 loadAnimator()方法加载你的XML动画资源,然后调用setTarget()应用到具备这个属性的目标对象上(譬如TextView)。
android:valueTo float、int或者color类型,必须要设置的节点属性,表明动画结束的点;如果是颜色的话,由6位十六进制的数字表示。
android:valueFrom 相对应valueTo,动画的起始点,如果没有指定,系统会通过属性的get方法获取,颜色也是6位十六进制的数字表示。
android:duration 动画的时长,int类型,以毫秒为单位,默认为300毫秒。
android:startOffset 动画延迟的时间,从调用start方法后开始计算,int型,毫秒为单位。
android:repeatCount 一个动画的重复次数,int型,”-1“表示无限循环,”1“表示动画在*次执行完成后重复执行一次,也就是两次,默认为0,不重复执行。
android:repeatMode 重复模式:int型,当一个动画执行完的时候应该如何处理。该值必须是正数或者是-1,“reverse”会使得按照动画向相反的方向执行,可实现类似钟摆效果。“repeat”会使得动画每次都从头开始循环。
android:valueType 关键参数,如果该value是一个颜色,那么就不需要指定,因为动画框架会自动的处理颜色值。有intType和floatType(默认)两种:分别说明动画值为int和float型。

<objectAnimator>属性解释:
同上<objectAnimator>属性,不多介绍。

XML属性动画使用方法:

AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(myContext,
    R.animtor.property_animator);
set.setTarget(myObject);
set.start();

 

4-2-3 Java方式属性动画

1、ObjectAnimator:继承自ValueAnimator,允许你指定要进行动画的对象以及该对象 的一个属性。该类会根据计算得到的新值自动更新属性。大多数的情况使用ObjectAnimator就足够了,因为它使得目标对象动画值的处理过程变得足 够简单,不用像ValueAnimator那样自己写动画更新的逻辑,但是ObjectAnimator有一定的限制,比如它需要目标对象的属性提供指定 的处理方法(譬如提供getXXX,setXXX方法),这时候你就需要根据自己的需求在ObjectAnimator和ValueAnimator中看 哪种实现更方便了。

ObjectAnimator类提供了ofInt、ofFloat、ofObject这个三个常用的方法,这些方法都是设置动画作用的元素、属性、开始、结束等任意属性值。当属性值(上面方法的参数)只设置一个时就把通过getXXX反射获取的值作为起点,设置的值作为终点;如果设置两个(参数),那么一个是开始、另一个是结束。

特别注意:ObjectAnimator的动画原理是不停的调用setXXX方法更新属性值,所有使用ObjectAnimator更新属性时的前提是Object必须声明有getXXX和setXXX方法。

我们通常使用ObjectAnimator设置View已知的属性来生成动画,而一般View已知属性变化时都会主动触发重绘图操作,所以动画会自 动实现;但是也有特殊情况,譬如作用Object不是View,或者作用的属性没有触发重绘,或者我们在重绘时需要做自己的操作,那都可以通过如下方法手 动设置:

ObjectAnimator mObjectAnimator= ObjectAnimator.ofInt(view, "customerDefineAnyThingName", 0,  1).setDuration(2000);
mObjectAnimator.addUpdateListener(new AnimatorUpdateListener()
        {
            @Override
            public void onAnimationUpdate(ValueAnimator animation)
            {
                //int value = animation.getAnimatedValue();  可以获取当前属性值
                //view.postInvalidate();  可以主动刷新
                //view.setXXX(value);
                //view.setXXX(value);
                //......可以批量修改属性
            }
        });

 

如下是一个我在项目中的Y轴3D旋转动画实现实例:

ObjectAnimator.ofFloat(view, "rotationY", 0.0f, 360.0f).setDuration(1000).start();

 

2、PropertyValuesHolder:多属性动画同时工作管理类。有时候我们需要同时修改多个属性,那就可以用到此类,具体如下:

PropertyValuesHolder a1 = PropertyValuesHolder.ofFloat("alpha", 0f, 1f);  
PropertyValuesHolder a2 = PropertyValuesHolder.ofFloat("translationY", 0, viewWidth);  
......
ObjectAnimator.ofPropertyValuesHolder(view, a1, a2, ......).setDuration(1000).start();

 

如上代码就可以实现同时修改多个属性的动画啦。

3、ValueAnimator:属性动画中的时间驱动,管理着动画时间的开始、结束属性值,相应时间属性值计算方法等。包含所有计算动画值的核心函数以及每一个动画时间节点上的信息、一个动画是否重复、是否监听更新事件等,并且还可以设置自定义的计算类型。

特别注意:ValueAnimator只是动画计算管理驱动,设置了作用目标,但没有设置属性,需要通过updateListener里设置属性才会生效。
ValueAnimator animator = ValueAnimator.ofFloat(0, mContentHeight);  //定义动画
animator.setTarget(view);   //设置作用目标
animator.setDuration(5000).start();
animator.addUpdateListener(new AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation){
        float value = (float) animation.getAnimatedValue();
        view.setXXX(value);  //必须通过这里设置属性值才有效
        view.mXXX = value;  //不需要setXXX属性方法
    }
});

 

大眼看上去可以发现和ObjectAnimator没啥区别,实际上正是由于ValueAnimator不直接操作属性值,所以要操作对象的属性可以不需要setXXX与getXXX方法,你完全可以通过当前动画的计算去修改任何属性。

4、AnimationSet:动画集合,提供把多个动画组合成一个组合的机制,并可设置动画的时序关系,如同时播放、顺序播放或延迟播放。具体使用方法比较简单,如下:

ObjectAnimator a1 = ObjectAnimator.ofFloat(view, "alpha", 1.0f, 0f);  
ObjectAnimator a2 = ObjectAnimator.ofFloat(view, "translationY", 0f, viewWidth);  
......
AnimatorSet animSet = new AnimatorSet();  
animSet.setDuration(5000);  
animSet.setInterpolator(new LinearInterpolator());   
//animSet.playTogether(a1, a2, ...); //两个动画同时执行  
animSet.play(a1).after(a2); //先后执行
......//其他组合方式
animSet.start();  

 

5、Evaluators相关类解释: Evaluators就是属性动画系统如何去计算一个属性值。它们通过Animator提供的动画的起始和结束值去计算一个动画的属性值。

  • IntEvaluator:整数属性值。
  • FloatEvaluator:浮点数属性值。
  • ArgbEvaluator:十六进制color属性值。
  • TypeEvaluator:用户自定义属性值接口,譬如对象属性值类型不是int、float、color类型,你必须实现这个接口去定义自己的数据类型。

既然说到这了,那就来个例子吧,譬如我们需要实现一个自定义属性类型和计算规则的属性动画,如下类型float[]:

ValueAnimator valueAnimator = new ValueAnimator();
valueAnimator.setDuration(5000);
valueAnimator.setObjectValues(new float[2]); //设置属性值类型
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.setEvaluator(new TypeEvaluator<float[]>()
{
    @Override
    public float[] evaluate(float fraction, float[] startValue,
                            float[] endValue)
    {
        //实现自定义规则计算的float[]类型的属性值
        float[] temp = new float[2];
        temp[0] = fraction * 2;
        temp[1] = (float)Math.random() * 10 * fraction;
        return temp;
    }
});

valueAnimator.start();
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
{
    @Override
    public void onAnimationUpdate(ValueAnimator animation)
    {
        float[] xyPos = (float[]) animation.getAnimatedValue();
        view.setHeight(xyPos[0]);   //通过属性值设置View属性动画
        view.setWidth(xyPos[1]);    //通过属性值设置View属性动画
    }
});

6、Interpolators相关类解释:

  • AccelerateDecelerateInterolator:先加速后减速。
  • AccelerateInterpolator:加速。
  • DecelerateInterpolator:减速。
  • AnticipateInterpolator:先向相反方向改变一段再加速播放。
  • AnticipateOvershootInterpolator:先向相反方向改变,再加速播放,会超出目标值然后缓慢移动至目标值,类似于弹簧回弹。
  • BounceInterpolator:快到目标值时值会跳跃。
  • CycleIinterpolator:动画循环一定次数,值的改变为一正弦函数:Math.sin(2 * mCycles * Math.PI * input)。
  • LinearInterpolator:线性均匀改变。
  • OvershottInterpolator:*后超出目标值然后缓慢改变到目标值。
  • TimeInterpolator:一个允许自定义Interpolator的接口,以上都实现了该接口。

举个例子,就像系统提供的标准API一样,如下就是加速插值器的实现代码,我们自定义时也可以类似实现:

//开始很慢然后不断加速的插值器。
public class AccelerateInterpolator implements Interpolator {
    private final float mFactor;
    private final double mDoubleFactor;

    public AccelerateInterpolator() {
        mFactor = 1.0f;
        mDoubleFactor = 2.0;
    }

    ......

    //input  0到1.0。表示动画当前点的值,0表示开头,1表示结尾。
    //return  插值。值可以大于1超出目标值,也可以小于0突破低值。
    @Override
    public float getInterpolation(float input) {
        //实现核心代码块
        if (mFactor == 1.0f) {
            return input * input;
        } else {
            return (float)Math.pow(input, mDoubleFactor);
        }
    }
}

综上可以发现,我们可以使用现有系统提供标准的东东实现属性动画,也可以通过自定义继承相关接口实现自己的动画,只要实现上面提到的那些主要方法即可。

4-2-4 Java属性动画拓展之ViewPropertyAnimator动画

在Android API 12时,View中添加了animate方法,具体如下:

public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
     ......
     /**
     * This method returns a ViewPropertyAnimator object, which can be used to animate
     * specific properties on this View.
     *
     * @return ViewPropertyAnimator The ViewPropertyAnimator associated with this View.
     */
    public ViewPropertyAnimator animate() {
        if (mAnimator == null) {
            mAnimator = new ViewPropertyAnimator(this);
        }
        return mAnimator;
    }
    ......
}

可以看见通过View的animate()方法可以得到一个ViewPropertyAnimator的属性动画(有人说他没有继承Animator类,是的,他是成员关系,不是之前那种继承关系)。

ViewPropertyAnimator提供了一种非常方便的方法为View的部分属性设置动画(切记,是部分属性),它可以直接使用一个 Animator对象设置多个属性的动画;在多属性设置动画时,它比 上面的ObjectAnimator更加牛逼、高效,因为他会管理多个属性的invalidate方法统一调运触发,而不像上面分别调用,所以还会有一些 性能优化。如下就是一个例子:

myView.animate().x(0f).y(100f).start(); 

 

4-2-5 Java属性动画拓展之LayoutAnimator容器布局动画

Property动画系统还提供了对ViewGroup中View添加时的动画功能,我们可以用LayoutTransition对 ViewGroup中的View进行动画设置显示。LayoutTransition的动画效果都是设置给ViewGroup,然后当被设置动画的 ViewGroup中添加删除View时体现出来。该类用于当前布局容器中有View添加、删除、隐藏、显示等时候定义布局容器自身的动画和View的动 画,也就是说当在一个LinerLayout中隐藏一个View的时候,我们可以自定义 整个由于LinerLayout隐藏View而改变的动画,同时还可以自定义被隐藏的View自己消失时候的动画等。

我们可以发现LayoutTransition类中主要有五种容器转换动画类型,具体如下:

  • LayoutTransition.APPEARING:当View出现或者添加的时候View出现的动画。
  • LayoutTransition.CHANGE_APPEARING:当添加View导致布局容器改变的时候整个布局容器的动画。
  • LayoutTransition.DISAPPEARING:当View消失或者隐藏的时候View消失的动画。
  • LayoutTransition.CHANGE_DISAPPEARING:当删除或者隐藏View导致布局容器改变的时候整个布局容器的动画。
  • LayoutTransition.CHANGE:当不是由于View出现或消失造成对其他View位置造成改变的时候整个布局容器的动画。

XML方式使用系统提供的默认LayoutTransition动画:

我们可以通过如下方式使用系统提供的默认ViewGroup的LayoutTransition动画:

android:animateLayoutChanges=”true”

在ViewGroup添加如上xml属性默认是没有任何动画效果的,因为前面说了,该动画针对于ViewGroup内部东东发生改变时才有效,所以当我们设置如上属性然后调运ViewGroup的addView、removeView方法时就能看见系统默认的动画效果了。

还有一种就是通过如下方式设置:

android:layoutAnimation=”@anim/customer_anim”

 

通过这种方式就能实现很多吊炸天的动画。

Java方式使用系统提供的默认LayoutTransition动画:

在使用LayoutTransition时,你可以自定义这几种事件类型的动画,也可以使用默认的动画,总之*终都是通过 setLayoutTransition(LayoutTransition lt)方法把这些动画以一个LayoutTransition对象设置给一个ViewGroup。

譬如实现如上Xml方式的默认系统LayoutTransition动画如下:

mTransitioner = new LayoutTransition();
mViewGroup.setLayoutTransition(mTransitioner);

稍微再高端一点吧,我们来自定义这几类事件的动画,分别实现他们,那么你可以像下面这么处理:

mTransitioner = new LayoutTransition();
......
ObjectAnimator anim = ObjectAnimator.ofFloat(this, "scaleX", 0, 1);
......//设置更多动画
mTransition.setAnimator(LayoutTransition.APPEARING, anim);
......//设置更多类型的动画                mViewGroup.setLayoutTransition(mTransitioner);

到此通过LayoutTransition你就能实现类似小米手机计算器切换普通型和科学型的炫酷动画了。

 

5 Android动画总结

到此Android动画基本已经描述OK了,也就这么三大类,尤其是属性动画更加一筹。但是特别说一句,上面基本都没有提及到各种动画的 Listener接口,原因是这个玩意太简单,所以不提了,相信你会监听View的onClickListener就一定会触类旁通动画的 Listener方法的。有了这些基础相信无论是自定义控件时还是自定义动画时都会起到直接的指导参考作用。其实对于Android的动画实现远远不止现 在提到的这些,但是这些又是基础,所以后面还会写文章说说Android提供的其他动画参考工具类的。

现在我们继续沿用官方的对比,翻译一下这些动画的区别,具体如下(点我参看原文How Property Animation Differs from View Animation):

View动画:

View动画只能够为View添加动画,如果想为非View对象添加动画须自己实现;且View动画支持的种类很少;尤其是他改变的是View的绘制效果,View的属性没有改变,其位置与大小都不变; View动画代码量少,使用简单方便。

Property动画:

弥补了View动画的缺陷,你可以为一个对象的任意属性添加动画,对象自己的属性会被真的改变;当对象的属性变化的时候,属性动画会自动刷新屏幕;属性动画改变的是对象的真实属性,而且属性动画不止用于View,还可以用于任何对象。

好了,太不容易了!这一篇文章差点难产了,断断续续各种事,终于追求完美OK了,接下来正常更新。

2020年我国“三新”经济增加值相当于国内生产总值的比重为17.08%

  2020年,尽管受到突如其来的新冠肺炎疫情的巨大冲击和严峻复杂国际形势的影响,我国新产业新业态新商业模式仍继续保持增长。经核算,2020年我国“三新”经济增加值为169254亿元,比上年增长4.5%(未扣除价格因素,下同),比同期国内生产总值(GDP)现价增速高1.5个百分点;相当于GDP的比重为17.08%,比上年提高0.7个百分点。   分三次产业看,“三新”经济中,*产业增加值为7423亿元,比上年增长11.0%,占比为4.39%;第二产业增加值为73487亿元,比上年增长4.3%,占比为43.42%;第三产业增加值为88345亿元,比上年增长4.2%,占比为52.20%。 2020年“三新”经济增加值核算结果    增加值 (亿元) 比上年 增长(%) 构成 (%) “三新”经济 169254 4.5 100.00 *产业 7423 11.0 4.39 第二产业 73487 4.3 43.42 第三产业 88345 4.2 52.20 注: 1.如分项数据之和与总计数据不等,是由于数值修约误差所致。 2.比上年增长按现价计算。    附注:   1.指标解释   “三新”经济是以新产业、新业态、新商业模式为核心内容的经济活动的集合。   新产业指应用新科技成果、新兴技术而形成一定规模的新型经济活动。具体表现为:一是新技术应用产业化直接催生的新产业;二是传统产业采用现代信息技术形成的新产业;三是由于科技成果、信息技术推广应用,推动产业的分化、升级、融合而衍生出的新产业。   新业态指顺应多元化、多样化、个性化的产品或服务需求,依托技术创新和应用,从现有产业和领域中衍生叠加出的新环节、新链条、新活动形态。具体表现为:一是以互联网为依托开展的经营活动;二是商业流程、服务模式或产品形态的创新;三是提供更加灵活快捷的个性化服务。   新商业模式指为实现用户价值和企业持续盈利目标,对企业经营的各种内外要素进行整合和重组,形成高效并具有独特竞争力的商业运行模式。具体表现为:一是将互联网与产业创新融合;二是把硬件融入服务;三是提供消费、娱乐、休闲、服务的一站式服务。   “三新”经济增加值衡量的是一个国家(或地区)所有常住单位在一定时期内从事“三新”经济生产活动创造的增加值。   2.核算范围和分类   “三新”经济增加值的核算范围根据《新产业新业态新商业模式统计分类(2018)》确定。   “三新”经济增加值三次产业分类依据国家统计局2018年关于三次产业划分的规定制定。“三新”经济*产业是指具有“三新”经济特征的农、林、牧、渔业(不含农、林、牧、渔专业及辅助性活动);第二产业是指具有“三新”经济特征的采矿业(不含开采专业及辅助性活动),制造业(不含金属制品、机械和设备修理业),电力、热力、燃气及水生产和供应业;第三产业是指具有“三新”经济特征的、除*产业和第二产业以外的其他行业(不含国际组织)。   3.核算方法及资料来源   “三新”经济增加值利用全国经济普查资料、“三新”经济统计资料和国民经济核算资料,从生产方采用增加值率法、相关指标推算法等多种方法核算。   4.“三新”经济增加值相当于国内生产总值的比重   “三新”经济增加值相当于国内生产总值的比重,利用当年“三新”经济增加值和国内生产总值初步核算数计算。 

彻底理解ThreadLocal

ThreadLocal是什么

早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。

所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,因此造成线程局部变量没有在Java开发者中得到很好的普及。

ThreadLocal的接口方法

ThreadLocal类接口很简单,只有4个方法,我们先来了解一下:

  • void set(Object value)设置当前线程的线程局部变量的值。
  • public Object get()该方法返回当前线程所对应的线程局部变量。
  • public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
  • protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

值得一提的是,在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal<T>。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。

ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。我们自己就可以提供一个简单的实现版本:

 

[java] view plain copy
  1. package com.test;  
  2. public class TestNum {  
  3.     // ①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值  
  4.     private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {  
  5.         public Integer initialValue() {  
  6.             return 0;  
  7.         }
  8.     };
  9.     // ②获取下一个序列值  
  10.     public int getNextNum() {  
  11.         seqNum.set(seqNum.get() + 1);  
  12.         return seqNum.get();  
  13.     }
  14.     public static void main(String[] args) {  
  15.         TestNum sn = new TestNum();  
  16.         // ③ 3个线程共享sn,各自产生序列号  
  17.         TestClient t1 = new TestClient(sn);  
  18.         TestClient t2 = new TestClient(sn);  
  19.         TestClient t3 = new TestClient(sn);  
  20.         t1.start();
  21.         t2.start();
  22.         t3.start();
  23.     }
  24.     private static class TestClient extends Thread {  
  25.         private TestNum sn;  
  26.         public TestClient(TestNum sn) {  
  27.             this.sn = sn;  
  28.         }
  29.         public void run() {  
  30.             for (int i = 0; i < 3; i++) {  
  31.                 // ④每个线程打出3个序列值  
  32.                 System.out.println(“thread[” + Thread.currentThread().getName() + “] –> sn[”  
  33.                          + sn.getNextNum() + “]”);  
  34.             }
  35.         }
  36.     }
  37. }

 

通常我们通过匿名内部类的方式定义ThreadLocal的子类,提供初始的变量值,如例子中①处所示。TestClient线程产生一组序列号,在③处,我们生成3个TestClient,它们共享同一个TestNum实例。运行以上代码,在控制台上输出以下的结果:

 

thread[Thread-0] –> sn[1]
thread[Thread-1] –> sn[1]
thread[Thread-2] –> sn[1]
thread[Thread-1] –> sn[2]
thread[Thread-0] –> sn[2]
thread[Thread-1] –> sn[3]
thread[Thread-2] –> sn[2]
thread[Thread-0] –> sn[3]
thread[Thread-2] –> sn[3]

 

考察输出的结果信息,我们发现每个线程所产生的序号虽然都共享同一个TestNum实例,但它们并没有发生相互干扰的情况,而是各自产生独立的序列号,这是因为我们通过ThreadLocal为每一个线程提供了单独的副本。

 

Thread同步机制的比较

ThreadLocal和线程同步机制相比有什么优势呢?ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。

在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。

而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。

由于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用,代码清单 9 2就使用了JDK 5.0新的ThreadLocal<T>版本。

概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

Spring使用ThreadLocal解决线程安全问题我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,*大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。

一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程。

  同一线程贯通三层这样你就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有关联的对象引用到的都是同一个变量。

下面的实例能够体现Spring对有状态Bean的改造思路:

代码清单3 TestDao:非线程安全

 

[java] view plain copy
  1. package com.test;  
  2. import java.sql.Connection;  
  3. import java.sql.SQLException;  
  4. import java.sql.Statement;  
  5. public class TestDao {  
  6.     private Connection conn;// ①一个非线程安全的变量  
  7.     public void addTopic() throws SQLException {  
  8.         Statement stat = conn.createStatement();// ②引用非线程安全变量  
  9.         // …  
  10.     }
  11. }

 

 

由于①处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造:

代码清单4 TestDao:线程安全

 

[java] view plain copy
  1. package com.test;  
  2. import java.sql.Connection;  
  3. import java.sql.SQLException;  
  4. import java.sql.Statement;  
  5. public class TestDaoNew {  
  6.     // ①使用ThreadLocal保存Connection变量  
  7.     private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();  
  8.     public static Connection getConnection() {  
  9.         // ②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,  
  10.         // 并将其保存到线程本地变量中。  
  11.         if (connThreadLocal.get() == null) {  
  12.             Connection conn = getConnection();
  13.             connThreadLocal.set(conn);
  14.             return conn;  
  15.         } else {  
  16.             return connThreadLocal.get();// ③直接返回线程本地变量  
  17.         }
  18.     }
  19.     public void addTopic() throws SQLException {  
  20.         // ④从ThreadLocal中获取线程对应的Connection  
  21.         Statement stat = getConnection().createStatement();
  22.     }
  23. }

 

 

不同的线程在使用TopicDao时,先判断connThreadLocal.get()是否是null,如果是null,则说明当前线程还没有对应的Connection对象,这时创建一个Connection对象并添加到本地线程变量中;如果不为null,则说明当前的线程已经拥有了Connection对象,直接使用就可以了。这样,就保证了不同的线程使用线程相关的Connection,而不会使用其它线程的Connection。因此,这个TopicDao就可以做到singleton共享了。

当然,这个例子本身很粗糙,将Connection的ThreadLocal直接放在DAO只能做到本DAO的多个方法共享Connection时不发生线程安全问题,但无法和其它DAO共用同一个Connection,要做到同一事务多DAO共享同一Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。

 

ConnectionManager.java

[java] view plain copy
  1. package com.test;  
  2. import java.sql.Connection;  
  3. import java.sql.DriverManager;  
  4. import java.sql.SQLException;  
  5. public class ConnectionManager {  
  6.     private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {  
  7.         @Override  
  8.         protected Connection initialValue() {  
  9.             Connection conn = null;  
  10.             try {  
  11.                 conn = DriverManager.getConnection(
  12.                         “jdbc:mysql://localhost:3306/test”, “username”,  
  13.                         “password”);  
  14.             } catch (SQLException e) {  
  15.                 e.printStackTrace();
  16.             }
  17.             return conn;  
  18.         }
  19.     };
  20.     public static Connection getConnection() {  
  21.         return connectionHolder.get();  
  22.     }
  23.     public static void setConnection(Connection conn) {  
  24.         connectionHolder.set(conn);
  25.     }
  26. }

 

 

java.lang.ThreadLocal<T>的具体实现

那么到底ThreadLocal类是如何实现这种“为每个线程提供不同的变量拷贝”的呢?先来看一下ThreadLocal的set()方法的源码是如何实现的:

 

[java] view plain copy
  1. /** 
  2.     * Sets the current thread’s copy of this thread-local variable 
  3.     * to the specified value.  Most subclasses will have no need to 
  4.     * override this method, relying solely on the {@link #initialValue} 
  5.     * method to set the values of thread-locals. 
  6.     * 
  7.     * @param value the value to be stored in the current thread’s copy of 
  8.     *        this thread-local. 
  9.     */  
  10.    public void set(T value) {  
  11.        Thread t = Thread.currentThread();
  12.        ThreadLocalMap map = getMap(t);
  13.        if (map != null)  
  14.            map.set(this, value);  
  15.        else  
  16.            createMap(t, value);
  17.    }

在这个方法内部我们看到,首先通过getMap(Thread t)方法获取一个和当前线程相关的ThreadLocalMap,然后将变量的值设置到这个ThreadLocalMap对象中,当然如果获取到的ThreadLocalMap对象为空,就通过createMap方法创建。

线程隔离的秘密,就在于ThreadLocalMap这个类。ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。还有一点就是,ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。

为了加深理解,我们接着看上面代码中出现的getMap和createMap方法的实现:

 

[java] view plain copy
  1. /** 
  2.  * Get the map associated with a ThreadLocal. Overridden in 
  3.  * InheritableThreadLocal. 
  4.  * 
  5.  * @param  t the current thread 
  6.  * @return the map 
  7.  */  
  8. ThreadLocalMap getMap(Thread t) {
  9.     return t.threadLocals;  
  10. }
  11. /** 
  12.  * Create the map associated with a ThreadLocal. Overridden in 
  13.  * InheritableThreadLocal. 
  14.  * 
  15.  * @param t the current thread 
  16.  * @param firstValue value for the initial entry of the map 
  17.  * @param map the map to store. 
  18.  */  
  19. void createMap(Thread t, T firstValue) {  
  20.     t.threadLocals = new ThreadLocalMap(this, firstValue);  
  21. }

接下来再看一下ThreadLocal类中的get()方法:

 

[java] view plain copy
  1. /** 
  2.  * Returns the value in the current thread’s copy of this 
  3.  * thread-local variable.  If the variable has no value for the 
  4.  * current thread, it is first initialized to the value returned 
  5.  * by an invocation of the {@link #initialValue} method. 
  6.  * 
  7.  * @return the current thread’s value of this thread-local 
  8.  */  
  9. public T get() {  
  10.     Thread t = Thread.currentThread();
  11.     ThreadLocalMap map = getMap(t);
  12.     if (map != null) {  
  13.         ThreadLocalMap.Entry e = map.getEntry(this);  
  14.         if (e != null)  
  15.             return (T)e.value;  
  16.     }
  17.     return setInitialValue();  
  18. }

再来看setInitialValue()方法:

 

[java] view plain copy
  1. /** 
  2.     * Variant of set() to establish initialValue. Used instead 
  3.     * of set() in case user has overridden the set() method. 
  4.     * 
  5.     * @return the initial value 
  6.     */  
  7.    private T setInitialValue() {  
  8.        T value = initialValue();
  9.        Thread t = Thread.currentThread();
  10.        ThreadLocalMap map = getMap(t);
  11.        if (map != null)  
  12.            map.set(this, value);  
  13.        else  
  14.            createMap(t, value);
  15.        return value;  
  16.    }

获取和当前线程绑定的值时,ThreadLocalMap对象是以this指向的ThreadLocal对象为键进行查找的,这当然和前面set()方法的代码是相呼应的。

进一步地,我们可以创建不同的ThreadLocal实例来实现多个变量在不同线程间的访问隔离,为什么可以这么做?因为不同的ThreadLocal对象作为不同键,当然也可以在线程的ThreadLocalMap对象中设置不同的值了。通过ThreadLocal对象,在多线程中共享一个值和多个值的区别,就像你在一个HashMap对象中存储一个键值对和多个键值对一样,仅此而已。

 

小结

ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

ConnectionManager.java

[java] view plain copy
  1. package com.test;  
  2. import java.sql.Connection;  
  3. import java.sql.DriverManager;  
  4. import java.sql.SQLException;  
  5. public class ConnectionManager {  
  6.     private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {  
  7.         @Override  
  8.         protected Connection initialValue() {  
  9.             Connection conn = null;  
  10.             try {  
  11.                 conn = DriverManager.getConnection(
  12.                         “jdbc:mysql://localhost:3306/test”, “username”,  
  13.                         “password”);  
  14.             } catch (SQLException e) {  
  15.                 e.printStackTrace();
  16.             }
  17.             return conn;  
  18.         }
  19.     };
  20.     public static Connection getConnection() {  
  21.         return connectionHolder.get();  
  22.     }
  23.     public static void setConnection(Connection conn) {  
  24.         connectionHolder.set(conn);
  25.     }
  26. }

后记

看到网友评论的很激烈,甚至关于ThreadLocalMap不是ThreadLocal里面的,而是Thread里面的这种评论都出现了,于是有了这个后记,下面先把jdk源码贴上,源码*有说服力了。

[java] view plain copy
  1. /** 
  2.      * ThreadLocalMap is a customized hash map suitable only for 
  3.      * maintaining thread local values. No operations are exported 
  4.      * outside of the ThreadLocal class. The class is package private to 
  5.      * allow declaration of fields in class Thread.  To help deal with 
  6.      * very large and long-lived usages, the hash table entries use 
  7.      * WeakReferences for keys. However, since reference queues are not 
  8.      * used, stale entries are guaranteed to be removed only when 
  9.      * the table starts running out of space. 
  10.      */  
  11.     static class ThreadLocalMap {…}  

源码就是以上,这源码自然是在ThreadLocal里面的,有截图为证。%title插图%num

 

本文是自己在学习ThreadLocal的时候,一时兴起,深入看了源码,思考了此类的作用、使用范围,进而联想到对传统的synchronize共享变量线程安全的问题进行比较,而总结的博文,总结一句话就是一个是锁机制进行时间换空间,一个是存储拷贝进行空间换时间。

关于Android TaskAffinity的那些事儿

关于Android TaskAffinity的那些事儿

正常情况下,如果应用已经启动,并将应用切到后台,在通知栏中调起页面时,该应用的Task首先会被调起,然后会将我们的Activity显示在这个Task的顶端。手机百度的通知栏里面有一个快速搜索栏,无论什么情况下,点击之后都会直接弹出搜索页面,透明背景后显示的是桌面。怎么来实现这个功能呢?这就要提到我们的主角TaskAffinity了。

什么是affinity?

affinity是指Activity的归属,Activity与Task的吸附关系,也就是该Activity属于哪个Task。一般情况下在同一个应用中,启动的Activity都在同一个Task中,它们在该Task中度过自己的生命。每个Activity都有taskAffinity属性,这个属性指出了它希望进入的Task。如果一个Activity没有显式的指明taskAffinity,那么它的这个属性就等于Application指明的taskAffinity,如果Application也没有指明,那么该taskAffinity的值就等于应用的包名。我们可以通过在元素中增加taskAffinity属性来为某一个Activity指定单独的affinity。这个属性的值是一个字符串,可以指定为任意字符串,但是必须至少包含一个”.”,否则会报错。

affinity在什么场合应用呢?

1.根据affinity重新为Activity选择宿主task(与allowTaskReparenting属性配合使用)

allowTaskReparenting用来标记Activity能否从启动的Task移动到taskAffinity指定的Task,当把Activity的allowTaskReparenting属性设置成true时,Activity就拥有了一个转移所在Task的能力。具体点来说,就是一个Activity现在是处于某个Task当中的,但是它与另外一个Task具有相同的affinity值,那么当另外这个任务切换到前台的时候,该Activity就可以转移到现在的这个任务当中。allowTaskReparenting默认是继承至application中的allowTaskReparenting=false,如果为true,则表示可以更换;false表示不可以。
举一个形象点的例子,比如有一个天气预报程序,它有一个用于显示天气信息的Activity,allowTaskReparenting属性设置成true,这个Activity和天气预报程序的所有其它Activity具体相同的affinity值。这个时候,你自己的应用程序通过Intent去启动了这个用于显示天气信息的Activity,那么此时这个Activity应该是和你的应用程序是在同一个任务当中的。但是当把天气预报程序切换到前台的时候,这个Activity会被转移到天气预报程序的任务当中,并显示出来。如果将你自己的应用切换到前台,发现你自己应用Task里的那个Activity消失了。

2.启动一个Activity过程中Intent使用了FLAG_ACTIVITY_NEW_TASK标记,根据affinity查找或创建一个新的具有对应affinity的task。

当调用startActivity()方法来启动一个Activity时,默认是将它放入到当前的任务当中。但是,如果在Intent中加入了FLAG_ACTIVITY_NEW_TASK flag的话,情况就会变的复杂起来。首先,系统会去检查这个Activity的affinity是否与当前Task的affinity相同。如果相同的话就会把它放入到当前Task当中,如果不同则会先去检查是否已经有一个名字与该Activity的affinity相同的Task,如果有,这个Task将被调到前台,同时这个Activity将显示在这个Task的顶端;如果没有的话,系统将会尝试为这个Activity创建一个新的Task。需要注意的是,如果一个Activity在manifest文件中声明的启动模式是”singleTask”,那么他被启动的时候,行为模式会和前面提到的指定FLAG_ACTIVITY_NEW_TASK一样。
那么,有了上面的知识,我们应该可以实现开头提到的功能了。

功能的实现

首先,在mainifest中配置我们的Activity,

       <activity
        android:name="com.test.TestActivity"
        android:configChanges="orientation|keyboard|keyboardHidden"
        android:exported="true"
        android:taskAffinity="com.test.TestActivity"
        android:screenOrientation="portrait"/>

然后增加通知栏的逻辑

  NotificationManager mNotifManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
  Notification notification = new Notification();
  notification.icon = R.drawable.icon;
  notification.flags = Notification.FLAG_ONGOING_EVENT;
  notification.flags = Notification.FLAG_AUTO_CANCEL;
  notification.flags = Notification.FLAG_NO_CLEAR;
  RemoteViews mContentView = new RemoteViews(mContext.getPackageName(), 
  R.layout.notification_test);
  notification.contentView = mContentView;

  Intent intent = new Intent();
  intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  intent.setClass(mContext, TestActivity.class);
  PendingIntent pendingIntent =PendingIntent.getActivity(mContext, 0, intent, 
  PendingIntent.FLAG_UPDATE_CURRENT);                     
     notification.contentView.setOnClickPendingIntent(R.id.rl_notification, 
  pendingIntent);

  mNotifManager.notify(NOTIFYID, notification);

现在我们可以实现开头提到的那种效果了。但是,我发现*近任务中会有两个我们应用的图标,看起来像是启动了两个我们的应用,非常奇怪,而实际上是因为我们的应用启动了两个Task。我们可以通过在manifest此Activity的属性中增加Android:excludeFromRecents=”true”。这属性用于设置由该Activity所启动的任务是否应该被排除在*近使用的应用程序列表之外。也就是说,当这个Activity是一个新任务的根节点时,这个属性决定了这个任务是否会显示在用户*近使用的应用程序列表中。如果设置为true,则这个任务会被排除在列表之外,为false,则表示会包含在*近使用的应用列表中。默认值是false。

场景验证

前提:将页面背景设置为半透明。
1、未使用affinity
如果应用没有启动,点击通知栏,当前Activity被调起,透明背景后显示为桌面。
如果应用已经启动,点击通知栏,应用的Task调到前台,当前Activy显示在该Task的顶端。
2、使用affinity
无论应用是否已经启动,点击通知栏,当前Activity都会被调起,透明背景后显示为桌面。
两种情况下,看*近任务栏,都只显示一个应用图标。
这样就实现了我们想要的效果。

Android 8.0 PictureInPicture 画中画模式分析与使用

之前的直播业务中有退出直播后显示一个小窗口继续播放视频直播需求,当时是用的windowManger做的windowManger实现连接,今天来了解一下Android8.0后的画中画怎么实现,先看效果图

 

先看一下谷歌文档的介绍

多窗口支持

Android 7.0 新增了对同时显示多个应用窗口的支持。在手持设备上,两个应用可以在分屏模式下左右并排或上下并排显示。在电视设备上,应用可以使用画中画模式,在用户与另一个应用互动的同时继续播放视频。

如果您的应用以 Android 7.0(API 级别 24)或更高版本为目标平台,则您可以配置应用处理多窗口显示的方式。例如,您可以指定 Activity 允许的*小尺寸。您还可以为自己的应用停用多窗口显示,确保系统仅以全屏模式显示该应用。

Android 允许多个应用同时共享屏幕。例如,用户可以分屏显示应用,在左边查看网页,同时在右边写电子邮件。用户体验取决于 Android 操作系统的版本和设备类型:

  • 搭载 Android 7.0 的手持设备支持分屏模式。在此模式下,系统以左右并排或上下并排的方式分屏显示两个应用。用户可以拖动两个应用之间的分界线,放大其中一个应用,同时缩小另一个应用。
  • 从 Android 8.0 开始,应用可以将自身置于画中画模式,从而使其能在用户浏览其他应用或与之互动时继续显示内容。
  • 更大尺寸设备的制造商可选择启用自由窗口模式,在该模式下,用户可以自由调整各 Activity 的大小。如果制造商启用此功能,设备将同时具有自由窗口模式和分屏模式。

%title插图%num

图 1. 两个应用在分屏模式下左右并排显示。

用户可以通过以下方式切换到多窗口模式:

  • 如果用户打开概览屏幕并长按 Activity 名称,则可以拖动该 Activity 至屏幕突出显示的区域,使 Activity 进入多窗口模式。
  • 如果用户长按“概览”按钮,设备上的当前 Activity 将进入多窗口模式,同时将打开概览屏幕,供用户选择要共享屏幕的另一个 Activity。

当两个 activity 共享屏幕时,用户可在二者之间拖放数据。

多窗口模式不会更改 activity 生命周期。

在多窗口模式下,在任意时间点都只有用户*近一次互动的 Activity 处于活动状态。此 Activity 被视为*顶层的 Activity,而且是唯一处于 RESUMED 状态的 Activity。所有其他可见的 Activity 均处于 STARTED 而非 RESUMED 状态。不过,这些可见但并不处于 RESUMED 状态的 Activity 在系统中享有比不可见 Activity 更高的优先级。如果用户与其中一个可见的 Activity 互动,那么该 Activity 将进入 RESUMED 状态,而之前的*顶层 Activity 将进入 STARTED 状态。

注意:在多窗口模式下,即使应用对用户可见,也可能不处于 RESUMED 状态。尽管不在*顶层,应用可能也需要继续执行其操作。例如,处于此状态的视频播放应用应继续显示其视频。出于此原因,我们建议播放视频的 Activity 不要通过暂停视频播放来响应 ON_PAUSE 生命周期事件。相反,此类 activity 应通过开始播放来响应 ON_START,通过暂停播放来响应 ON_STOP。如果您未使用生命周期软件包,而是直接处理生命周期事件,请在您的 onStop() 处理程序中暂停视频播放,在 onStart()中继续视频播放。

如处理配置更改中所述,当用户将应用置于多窗口模式下时,系统会通知 activity 发生的配置更改。当用户调整应用大小或将应用恢复到全屏模式时,系统也会通知发生了配置更改。该更改与系统通知应用设备已从纵向模式切换到横屏模式时的 Activity 生命周期影响基本相同,区别在于设备不仅仅是切换了方向,还更改了尺寸。如处理配置更改中所述,您的 Activity 可以自行处理配置更改,或允许系统销毁 Activity,并以新的尺寸重新创建该 Activity。

如果用户调整窗口大小并在任意维度放大窗口,那么系统会根据用户操作调整 activity 的大小,同时根据需要发出配置更改通知。如果应用在新公开区域的绘制滞后,系统会使用 windowBackground属性或默认 windowBackgroundFallback 样式属性指定的颜色临时填充这些区域。

当应用处于多窗口模式时,恢复状态取决于设备的 Android 版本。

  • 在 Android 9(API 级别 28)及更低版本中,只有获得焦点的 activity 处于 RESUMED 状态,所有其他 activity 均为 PAUSED 状态。如果一个应用进程中有多个 activity,则 Z 轴顺序*高的 activity 为 RESUMED 状态,其他 activity 为 PAUSED 状态。
  • 在 Android 10(API 级别 29)及更高版本中,所有顶层的可聚焦 activity 均处于 RESUMED 状态。

如需了解详情,请参阅多项恢复文档。

如果您的应用以 API 级别 24 或更高级别为目标平台,那么您可以配置该应用的 activity 是否支持以及如何支持多窗口显示。您可以在清单中设置属性来控制大小和布局。根 Activity 的属性设置会应用于其任务堆栈中的所有 Activity。例如,如果根 Activity 将 android:resizeableActivity 设置为 true,那么任务堆栈中的所有 Activity 均可调整大小。

注意:如果您构建以 API 级别 23 或更低级别为目标平台、支持多个屏幕方向的应用,那么当用户在多窗口模式下使用应用时,系统会强制调整应用大小。系统会显示一个对话框,提醒用户应用可能会出现意外行为。系统不会调整定向应用的大小;如果用户尝试在多窗口模式下打开一个定向应用,该应用将全屏显示。

在 Chromebook 等一些较大的设备上,即使指定了 android:resizeableActivity=”false”,您的应用也可能会在可调整大小的窗口中运行。如果这会破坏您的应用,您可以使用过滤条件来限制您的应用在此类设备上的可用性。

在清单的 <activity> 或 <application> 元素中设置此属性,以启用或停用多窗口显示:

android:resizeableActivity=["true" | "false"]

如果将此属性设置为 true,则·Activity 能以分屏和自由窗口模式启动。如果将此属性设置为 false,则 Activity 不支持多窗口模式。如果此值为 false,并且用户尝试在多窗口模式下启动 Activity,则 Activity 会全屏显示。

如果您的应用以 API 级别 24 为目标平台,但您未指定此属性的值,则其值默认设为 true。

在清单的 <activity> 节点中设置此属性,以指示 Activity 是否支持画中画显示。如果 android:resizeableActivity 为 false,系统将忽略此属性。

android:supportsPictureInPicture=["true" | "false"]

在 Android 7.0 中,<layout> 清单元素支持以下几种属性,这些属性会影响 Activity 在多窗口模式下的行为:

android:defaultWidth
在自由窗口模式下启动时 Activity 的默认宽度。
android:defaultHeight
在自由窗口模式下启动时 Activity 的默认高度。
android:gravity
在自由窗口模式下启动时 activity 的初始位置。请参阅 Gravity 参考资料,了解合适的值设置。
android:minHeightandroid:minWidth
分屏和自由窗口模式下 Activity 的*小高度和*小宽度。如果用户在分屏模式下移动分界线,使 Activity 尺寸小于指定的*小值,则系统会将 Activity 裁剪为用户请求的尺寸。

例如,以下代码展示了如何指定 Activity 在自由窗口模式下显示时的默认大小、位置和*小尺寸:

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

如果您自行处理多窗口配置更改(例如当用户调整窗口大小时会怎样),请将至少指定以下值的 android:configChanges 属性添加到清单中:

<activity
  android:name=".MyActivity"
  android:configChanges="screenSize|smallestScreenSize
      |screenLayout|orientation"
/>

添加 android:configChanges 后,您的 Activity 和 Fragment 会收到对 onConfigurationChanged() 的回调,而不是被销毁并重新创建。然后,您可以根据需要手动更新视图、重新加载资源以及执行其他操作。

从 Android 7.0 开始,系统会为应用提供支持其在多窗口模式下运行的功能。

当设备处于多窗口模式时,系统会停用或忽略某些功能,因为对于可能与其他 Activity 或应用共享设备屏幕的 Activity 而言,这些功能并没有任何意义。此类功能包括:

  • 某些系统界面自定义选项将被停用;例如,在非全屏模式下,应用无法隐藏状态栏。
  • 系统将忽略对 android:screenOrientation 属性所作的更改。

Activity 提供以下方法来支持多窗口显示。

isInMultiWindowMode()
调用该方法可确认 activity 是否处于多窗口模式。
isInPictureInPictureMode()
调用该方法可确认 Activity 是否处于画中画模式。

注意:画中画模式是多窗口模式的特例。如果 myActivity.isInPictureInPictureMode()返回 true,那么 myActivity.isInMultiWindowMode() 也会返回 true。

onMultiWindowModeChanged()
每当 Activity 进入或退出多窗口模式时,系统都会调用此方法。当 Activity 进入多窗口模式时,系统会向该方法传递 true 值,退出多窗口模式时则传递 false 值。
onPictureInPictureModeChanged()
每当 Activity 进入或退出画中画模式时,系统都会调用此方法。当 activity 进入画中画模式时,系统会向该方法传递 true 值,退出画中画模式时则传递 false 值。

对于以上的很多方法,Fragment 类公开了多个版本,例如 Fragment.onMultiWindowModeChanged()

如要将 Activity 置于画中画模式,请调用 Activity.enterPictureInPictureMode()。如果设备不支持画中画模式,则调用此方法不会产生任何影响。如需了解详细信息,请参阅画中画文档。

启动新 activity 时,您可以指示应尽可能将新 activity 显示在当前 activity 旁边。为此,请使用 Intent 标志 FLAG_ACTIVITY_LAUNCH_ADJACENT。此标志将告知系统尽量在启动它的 Activity 旁边创建新 Activity,以便两个 Activity 共享屏幕。系统会尽可能这些做,但*终结果无法保证。

如果设备处于自由窗口模式,则在启动新 Activity 时,您可通过调用 ActivityOptions.setLaunchBounds() 来指定新 Activity 的尺寸和屏幕位置。如果设备未处于多窗口模式,则调用该方法不会产生任何影响。

注意:如果您在任务堆栈中启动 Activity,该 Activity 会替换屏幕上的 Activity,并沿用其所有的多窗口模式属性。如果您要在多窗口模式下以单独的窗口启动新 activity,那么必须在新的任务堆栈中启动此 activity。

当两个 activity 共享屏幕时,用户可在二者之间拖放数据。(在 Android 7.0 之前,用户只能在一个 activity 内拖放数据。)如需快速添加对在可编辑的 TextView 微件中接收拖动内容的支持,请参阅 Jetpack 中的 OnReceiveContentListener。如需添加全面的拖放支持(例如能够从您的应用拖动内容),请参阅拖放主题。

DragAndDropPermissions
令牌对象,负责指定向接收拖放数据的应用授予的权限。
View.startDragAndDrop()
等同于 View.startDrag()。如要启用跨 activity 拖放,请传递 DRAG_FLAG_GLOBAL 标志。如需向接收拖放数据的 activity 授予 URI 权限,请根据情况传递 DRAG_FLAG_GLOBAL_URI_READ 或 DRAG_FLAG_GLOBAL_URI_WRITE 标志。
View.cancelDragAndDrop()
取消当前正在进行的拖动操作。只能由发起拖动操作的应用调用。
View.updateDragShadow()
替换当前正在进行的拖动操作的拖动阴影。只能由发起拖动操作的应用调用。
Activity.requestDragAndDropPermissions()
请求权限,以获取通过 DragEvent 中所含 ClipData 传递的内容 URI。
DragAndDropPermissions.release()
释放访问 ClipData 中提供的内容 URI 上的数据所需的权限。 如果您不调用此方法,则在系统销毁包含的 activity 时会自动释放权限。

每个根 activity 都有自己的任务,该任务在单独的进程中运行,并显示在其自己的窗口中。如需在单独的窗口中启动应用的新实例,可使用 FLAG_ACTIVITY_NEW_TASK 标志启动新 activity。您可以将此标志与某些多窗口模式属性结合使用,以请求用于新窗口的特定位置。例如,购物应用可以显示多个窗口来比较商品。

请不要将多实例与多面板布局混淆,例如使用 SlidingPaneLayout 的列表/详情布局。它在单个窗口内运行。

请注意,当多个实例在可折叠设备上的单独窗口中运行时,如果折叠状态发生更改,则一个或多个实例可能会发送到后台。例如,假设一台设备已展开,并且有两个应用实例在折叠任一侧的两个窗口中运行。如果设备处于折叠状态,系统可能会终止其中的一个实例,而不是在较小的屏幕上尝试使窗口适应两个实例。

无论您的应用是否以 API 级别 24 或更高级别为目标平台,您都应验证应用在多窗口模式下的行为,以防用户尝试在搭载 Android 7.0 或更高版本的设备上以多窗口模式启动应用。

如果设备搭载 Android 7.0 或更高版本,将自动支持分屏模式。

如果您的应用以 API 级别 23 或更低级别为目标平台,那么当用户尝试在多窗口模式下使用应用时,系统将强制调整应用大小(除非应用进行定向声明)。

如果您的应用未进行定向声明,您应在搭载 Android 7.0 或更高版本的设备上启动应用,并尝试将应用置于分屏模式。确认应用被强制调整大小后,能提供可接受的用户体验。

如果您的应用进行定向声明,您应尝试将应用置于多窗口模式。确认执行此操作后,应用仍保持全屏模式。

如果您的应用以 API 级别 24 或更高级别为目标平台,并且未停用多窗口支持,请在分屏模式和自由窗口模式下验证以下行为。

  • 在全屏模式下启动应用,然后通过长按“概览”按钮切换到多窗口模式。确认应用能正常切换。
  • 直接在多窗口模式下启动应用,确认应用能正常启动。您可以按一下“概览”按钮,然后长按应用的名称栏,并将其拖动到屏幕上任一突出显示的区域,从而在多窗口模式下启动应用。
  • 拖动分界线,调整应用在分屏模式下的大小。确认应用正常调整大小且未崩溃,并且必要的界面元素仍然可见。
  • 如果您已指定应用的*小尺寸,请尝试将应用尺寸调整到*小值以下。确认无法将应用尺寸调整到指定*小值以下。
  • 完成所有测试后,确认应用性能可以接受。例如,确认调整应用大小后,更新界面没有长时间的滞后。

如要验证应用在多窗口模式下的性能,请执行以下操作。除非另有说明,否则应分别在分屏模式和多窗口模式下执行以下操作。

  • 进入和退出多窗口模式。
  • 从您的应用切换至另一个应用,确认应用在非活跃但可见的状态下正常运行。例如,如果您的应用正在播放视频,请确认当用户与另一个应用互动时,视频仍在继续播放。
  • 在分屏模式下,尝试移动分界线,放大和缩小您的应用。在左右并排和上下并排配置下,都要尝试放大和缩小操作。确认应用不会崩溃,主要功能可见,且调整大小的操作无需过长时间。
  • 快速连续地执行几次调整大小的操作。确认应用不会崩溃或出现内存泄露。如需了解如何查看应用的内存用量,请使用 Android Studio 的内存性能分析器。
  • 在多种不同的窗口配置下正常使用应用,确认应用能正常运行。确认文本可读,且界面元素大小正常,不影响互动。

如果您已通过设置 android:resizeableActivity="false" 停用多窗口支持,则应在运行 Android 7.0 或更高版本的设备上启动应用,并尝试将应用置于自由窗口模式和分屏模式。确认执行此操作后,应用仍保持全屏模式。

1,在清单文件AndroidManifest中声名允许开启画中画模式
android:resizeableActivity=”true” android:supportsPictureInPicture=”true”

<activity android:name=”.TestActivity”
android:configChanges=”screenSize|smallestScreenSize|screenLayout|orientation”
android:resizeableActivity=”true”
android:hardwareAccelerated=”true”
android:supportsPictureInPicture=”true”>
<intent-filter>
<action android:name=”android.intent.action.MAIN” />

<category android:name=”android.intent.category.LAUNCHER” />
</intent-filter>
</activity>
2.在activity中调用enterPictureInPictureMode()方法
/** Enters Picture-in-Picture mode. 进入画中画模式*/
void minimize() {
if (mMovieView == null) {
return;
}
// Hide the controls in picture-in-picture mode. 在画中画模式中 隐藏控制条
mMovieView.hideControls();
// Calculate the aspect ratio of the PiP screen. 计算屏幕的纵横比
Rational aspectRatio = new Rational(mMovieView.getWidth(), mMovieView.getHeight());
mPictureInPictureParamsBuilder.setAspectRatio(aspectRatio).build();
enterPictureInPictureMode(mPictureInPictureParamsBuilder.build());
}
该方法需要传PictureInPictureParams 参数,主要用于确定我们activity需要作为画中画的宽高比,我们可以将宽高设置为videoView的宽高,这样宽高比就与视频画面一致,进入画中画前隐藏其他UI控件就行了

3,在onPictureInPictureModeChanged方法中处理画中画切换的回调
@Override
public void onPictureInPictureModeChanged(
boolean isInPictureInPictureMode, Configuration configuration) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, configuration);
if (isInPictureInPictureMode) {
// Starts receiving events from action items in PiP mode.
//开始接收画中画模式的操作
mReceiver =
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null
|| !ACTION_MEDIA_CONTROL.equals(intent.getAction())) {
return;
}
// This is where we are called back from Picture-in-Picture action items.
//这就是我们从画中画模式的操作回调的地方
final int controlType = intent.getIntExtra(EXTRA_CONTROL_TYPE, 0);
switch (controlType) {
case CONTROL_TYPE_PLAY:
mMovieView.play();
break;
case CONTROL_TYPE_PAUSE:
mMovieView.pause();
break;
}
}
};
registerReceiver(mReceiver, new IntentFilter(ACTION_MEDIA_CONTROL));
} else {
// We are out of PiP mode. We can stop receiving events from it.
// 当我们不在画中画模式时,停止接收广播
unregisterReceiver(mReceiver);
mReceiver = null;
// Show the video controls if the video is not playing
//当视频不在播放时,显示控制条
if (mMovieView != null && !mMovieView.isPlaying()) {
mMovieView.showControls();
}
}
}
然后手势移动,关闭画中画,画中画切换回原页面等操作谷歌已经替我们做好了
需要注意画中画模式只有在Android8.0及以后才有,低版本实现画中画还是需要利用windowManger 通过addview去做
————————————————

Android允许以画中画 (PIP) 模式启动 Activity

Android 8.0(API 级别 26)允许以画中画 (PIP) 模式启动 Activity。画中画是一种特殊类型的多窗口模式,*常用于视频播放。使用该模式,用户可以通过固定到屏幕一角的小窗口观看视频,同时在应用之间进行导航或浏览主屏幕上的内容。

%title插图%num

画中画利用 Android 7.0 中的多窗口模式 API 来提供固定的视频叠加窗口。如要将画中画添加到您的应用中,您需要注册支持画中画的 Activity,根据需要将 Activity 切换为画中画模式,并确保当 Activity 处于画中画模式时,界面元素处于隐藏状态且视频能够继续播放。

画中画窗口会显示在屏幕的*上层,位于系统选择的一角。您可以将画中画窗口拖动到其他位置。当您点按该窗口时,会看到两个特殊的控件:全屏切换开关(位于窗口的中心)和关闭按钮(右上角的“X”)。

您的应用会控制当前 Activity 在何时进入画中画模式。下面是一些示例:

  • 一个 Activity 可以在用户点按主屏幕或*近使用的应用按钮来选择其他应用时,进入画中画模式。(Google 地图就是通过这种方式,在用户同时运行其他 Activity 时继续显示路线。)
  • 您的应用可以在用户从某个视频返回以浏览其他内容时,将该视频切换到画中画模式。
  • 您的应用可以在用户观看到某集内容结束时将视频切换到画中画模式。主屏幕会显示有关这部电视剧下一集的宣传信息或剧情摘要信息。
  • 您的应用可以提供一种方式,让用户可以在观看视频时将其他内容加入播放队列。当主屏幕显示内容选择 Activity 时,视频会继续以画中画模式播放。

默认情况下,系统不会自动为应用提供画中画支持。如果您想在应用中支持画中画,可以通过将 android:supportsPictureInPicture 设置为 true,在清单中注册视频 Activity。此外,指定您的 Activity 处理布局配置更改,这样一来,在画中画模式转换期间发生布局更改时,您的 Activity 就不会重新启动。

<activity android:name="VideoActivity"
    android:supportsPictureInPicture="true"
    android:configChanges=
        "screenSize|smallestScreenSize|screenLayout|orientation"
    ...

如要进入画中画模式,Activity 必须调用 enterPictureInPictureMode()。例如,以下代码会在用户点击应用界面中的专用按钮时,将 Activity 切换到画中画模式:

KotlinJava
@Override
public void onActionClicked(Action action) {
    if (action.getId() == R.id.lb_control_picture_in_picture) {
        getActivity().enterPictureInPictureMode();
        return;
    }
    ...
}

您可能需要添加将 Activity 切换到画中画模式(而不是进入后台)的逻辑。例如,如果用户在 Google 地图正在导航时按下主屏幕或*近使用的应用按钮,则该应用会切换到画中画模式。您可以通过替换 onUserLeaveHint() 来实现这一目的:

KotlinJava
@Override
public void onUserLeaveHint () {
    if (iWantToBeInPipModeNow()) {
        enterPictureInPictureMode();
    }
}

当 Activity 进入或退出画中画模式时,系统会调用 Activity.onPictureInPictureModeChanged() 或 Fragment.onPictureInPictureModeChanged()

您应替换这些回调以重新绘制 Activity 的界面元素。请注意,在画中画模式下,您的 Activity 会在一个小窗口中显示。在画中画模式下,用户无法与界面元素互动,并且可能很难看清小界面元素的详细信息。界面*简的视频播放 Activity 可提供*佳的用户体验。Activity 应仅显示视频播放控件。在 Activity 进入画中画模式之前移除其他界面元素,并在 Activity 再次变为全屏时恢复这些元素:

KotlinJava
@Override
public void onPictureInPictureModeChanged (boolean isInPictureInPictureMode, Configuration newConfig) {
    if (isInPictureInPictureMode) {
        // Hide the full-screen UI (controls, etc.) while in picture-in-picture mode.
    } else {
        // Restore the full-screen UI.
        ...
    }
}

画中画窗口可以在用户打开窗口菜单(通过点按移动设备上的窗口或使用电视遥控器选择菜单)时显示控件。

如果应用有处于活跃状态的媒体会话,则窗口会显示播放、暂停、下一个和上一个控件。

您还可以通过在进入画中画模式之前构建 PictureInPictureParams(使用 PictureInPictureParams.Builder.setActions())来明确指定自定义操作,并使用 enterPictureInPictureMode(android.app.PictureInPictureParams) 或 setPictureInPictureParams(android.app.PictureInPictureParams) 在进入画中画模式时传递这些参数。请注意,如果您尝试添加的控件数量超过 getMaxNumPictureInPictureActions(),则系统只会添加上限数量的控件。

当您的 Activity 切换到画中画模式时,系统会将该 Activity 置于暂停状态并调用 Activity 的 onPause() 方法。当 Activity 在画中画模式下暂停时,视频播放不得暂停,而应继续播放。

在 Android 7.0 及更高版本中,当系统调用 Activity 的 onStop() 时,您应暂停视频播放;当系统调用 Activity 的 onStart() 时,您应恢复视频播放。这样一来,您就无需在 onPause() 中检查应用是否处于画中画模式,只需继续播放视频即可。

如果您必须在 onPause() 实现中暂停播放,请通过调用 isInPictureInPictureMode() 检查是否处于画中画模式并相应地处理播放情况,例如:

KotlinJava
@Override
public void onPause() {
    // If called while in PIP mode, do not pause playback
    if (isInPictureInPictureMode()) {
        // Continue playback
        ...
    } else {
        // Use existing playback logic for paused Activity behavior.
        ...
    }
}

当您的 Activity 从画中画模式切换回全屏模式时,系统会恢复您的 Activity 并调用 onResume() 方法。

在您的应用中,可能会出现以下情况:有一个视频播放 Activity 正处于画中画模式,用户在主屏幕上浏览内容时选择了新的视频。应以全屏模式在现有的播放 Activity 中播放新的视频,而不是启动可能会令用户感到困惑的新 Activity。

如要确保将单个 Activity 用于视频播放请求并根据需要进入或退出画中画模式,请在清单中将 Activity 的 android:launchMode 设置为 singleTask

<activity android:name="VideoActivity"
    ...
    android:supportsPictureInPicture="true"
    android:launchMode="singleTask"
    ...

在您的 Activity 中,替换 onNewIntent() 并处理新的视频,从而根据需要停止任何现有的视频播放。

低内存设备可能无法使用画中画模式。在应用使用画中画之前,请务必通过调用 hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) 进行检查以确保可以使用画中画。

画中画旨在用于播放全屏视频的 Activity。将 Activity 切换到画中画模式时,请避免显示视频内容以外的任何内容。跟踪您的 Activity 何时进入画中画模式并隐藏界面元素,如处理画中画模式下的界面元素中所述。

由于画中画窗口在屏幕的一角显示为浮动窗口,因此您应避免在主屏幕中可能被画中画窗口遮盖的任何区域内显示重要信息。

当 Activity 进入画中画模式后,默认不会获得输入焦点。要在画中画模式下接收输入事件,请使用 MediaSession.setCallback()。如需详细了解如何使用 setCallback(),请参阅显示“正在播放”卡片。

当您的应用处于画中画模式时,画中画窗口中的视频播放可能会对其他应用(例如,音乐播放器应用或语音搜索应用)造成音频干扰。为避免出现此问题,请在开始播放视频时请求音频焦点,并处理音频焦点更改通知,如管理音频焦点中所述。如果您在处于画中画模式时收到音频焦点丢失通知,请暂停或停止视频播放。

当您的应用即将进入画中画模式时,请注意,只有顶层 Activity 才会进入画中画模式。在某些情况下(例如在多窗口设备上),此时系统可能会显示下层 Activity,在画中画 Activity 旁边,您可能会再次看到下层 Activity。您应根据情况相应地处理这一问题,包括下层 Activity 获取 onResume()或 onPause() 回调。用户也有可能与该 Activity 进行交互。例如,如果您的视频列表 Activity 正在显示,视频播放 Activity 处于画中画模式,用户可能会从列表中选择新视频,画中画 Activity 应相应地进行更新。

289. 生命游戏(JS实现)

289. 生命游戏(JS实现)
1 题目
根据 百度百科 ,生命游戏,简称为生命,是英国数学家约翰·何顿·康威在 1970 年发明的细胞自动机。
给定一个包含 m × n 个格子的面板,每一个格子都可以看成是一个细胞。每个细胞都具有一个初始状态:1 即为活细胞(live),或 0 即为死细胞(dead)。每个细胞与其八个相邻位置(水平,垂直,对角线)的细胞都遵循以下四条生存定律:
如果活细胞周围八个位置的活细胞数少于两个,则该位置活细胞死亡;
如果活细胞周围八个位置有两个或三个活细胞,则该位置活细胞仍然存活;
如果活细胞周围八个位置有超过三个活细胞,则该位置活细胞死亡;
如果死细胞周围正好有三个活细胞,则该位置死细胞复活;
根据当前状态,写一个函数来计算面板上所有细胞的下一个(一次更新后的)状态。下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的,其中细胞的出生和死亡是同时发生的。
示例:
输入:
[
[0,1,0],
[0,0,1],
[1,1,1],
[0,0,0]
]
输出:
[
[0,0,0],
[1,0,1],
[0,1,1],
[0,1,0]
]
进阶:
你可以使用原地算法解决本题吗?请注意,面板上所有格子需要同时被更新:你不能先更新某些格子,然后使用它们的更新后的值再更新其他格子。
本题中,我们使用二维数组来表示面板。原则上,面板是无限的,但当活细胞侵占了面板边界时会造成问题。你将如何解决这些问题?
链接:https://leetcode-cn.com/problems/game-of-life
2 思路
首先我们遍历整个矩阵,如果某细胞是活的,那么给周围8个位置细胞都加上影响因子10,然后我们第二次遍历矩阵,根据每个细胞的影响因子数来更新细胞的状态
3代码
/**
 * @param {number[][]} board
 * @return {void} Do not return anything, modify board in-place instead.
 */
var gameOfLife = function(board) {
    let m = board.length;
    let n = board[0].length;
    function affect(i,j) {
        if (board[i][j] % 10 === 0) return;
        for (let x=i-1; x<=i+1; x++) {
            for (let y=j-1; y<=j+1; y++) {
                if (x === i && y === j || x < 0 || y < 0 || x >= m || y >= n) continue;
                board[x][y] += 10;
            }
        }
    }
    function caculte(i,j) {
        let num = board[i][j];
        let k = Math.floor(num / 10);
        if (num % 10 === 0) {
            if ( k === 3) {
                board[i][j] = 1;
            } else {
                board[i][j] = 0;
            }
        } else {
            if (k < 2 || k > 3) {
                board[i][j] = 0;
            } else {
                board[i][j] = 1;
            }
        }
    }
    for (let i=0; i<m; i++) {
        for (let j=0; j<n; j++) {
            affect(i,j);
        }
    }
    for (let i=0; i<m; i++) {
        for (let j=0; j<n; j++) {
            caculte(i,j);
        }
    }
};

总结系列-Android画中画模式

*近做做播放器,有个浮窗播放的需求,两种实现方式,一种是申请浮窗权限,创建浮窗参考 flowWindow,一种是采用画中画模式(8.0以上)

关于画中画
Android 8.0 Oreo(API Level 26)允许活动启动画中画 Picture-in-picture(PIP)模式。PIP 是一种特殊类型的多窗口模式,主要用于视频播放。PIP 模式已经可用于 Android TV,而 Android 8.0 则让该功能可进一步用于其他 Android 设备。
画中画利用 Android 7.0 中的多窗口模式 API 来提供固定的视频叠加窗口。要将画中画添加到您的应用中,您需要注册支持画中画的 Activity、根据需要将 Activity 切换为画中画模式,并确保当 Activity 处于画中画模式时,界面元素处于隐藏状态且视频能够继续播放。

%title插图%num

如何使用
声明对画中画的支持
默认情况下,系统不会自动为应用提供画中画支持。要想在应用中支持画中画,您可以通过将 android:supportsPictureInPicture 和 android:resizeableActivity 设置为 true ,在清单中注册视频 Activity。此外,指定您的 Activity 会处理布局配置更改,这样一来,在画中画模式转换期间发生布局更改时,您的 Activity 不会重新启动。

<activity android:name=”VideoActivity”
android:resizeableActivity=”true”
android:supportsPictureInPicture=”true”
android:configChanges=
“screenSize|smallestScreenSize|screenLayout|orientation”

低内存设备可能无法使用画中画模式。在应用使用画中画之前,请务必通过调用 hasSystemFeature(PackageManager. FEATURE_PICTURE_IN_PICTURE) 进行检查以确保可以使用画中画。
isSupportPipMode = getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
if(videoPlayer!= null) {
videoPlayer.setSupportPipMode(isSupportPipMode);
}
对单个播放 Activity 使用画中画模式
要确保将单个 Activity 用于视频播放请求并根据需要进入或退出画中画模式,请在清单中将 Activity 的 android:launchMode 设置为 singleTask:
<activity android:name=”VideoActivity”

android:supportsPictureInPicture=”true”
android:launchMode=”singleTask”

在您的 Activity 中,替换 onNewIntent() 并处理新的视频,从而根据需要停止任何现有的视频播放。

将您的 Activity 切换到画中画模式
要进入画中画模式,Activity 必须调用 enterPictureInPictureMode()。例如,以下代码会在用户点击应用界面中的专用按钮时,将 Activity 切换到画中画模式:
/**
* 进入画中画模式
*/
private PictureInPictureParams.Builder mPictureInPictureParamsBuilder;
private void enterPiPMode() {
if (videoPlayer == null) {
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
videoPlayer.setIsInPictureInPictureMode(true);
if (mPictureInPictureParamsBuilder == null) {
mPictureInPictureParamsBuilder = new PictureInPictureParams.Builder();
}
// Calculate the aspect ratio of the PiP screen. 计算video的纵横比
mVideoWith = videoPlayer.getCurrentVideoWidth();
mVideoHeight = videoPlayer.getCurrentVideoHeight();
if (mVideoWith != 0 && mVideoHeight != 0) {
//设置param宽高比,根据宽高比例调整初始参数
Rational aspectRatio = new Rational(mVideoWith, mVideoHeight);
mPictureInPictureParamsBuilder.setAspectRatio(aspectRatio);
}
//进入pip模式
enterPictureInPictureMode(mPictureInPictureParamsBuilder.build());
}
}
进入 PIP 模式的*常见流程如下:
1. 从按钮触发
* onClicked (View),onOptionsItemSelected (MenuItem) 等等。
2. 有意的离开您的应用程序触发
* onUserLeaveHint ( )
3. 从返回触发
* onBackPressed ( )

在画中画期间处理界面
当 Activity 进入或退出画中画模式时,系统会调用 Activity. onPictureInPictureModeChanged() 或 Fragment. onPictureInPictureModeChanged()。
您应替换这些回调以重新绘制 Activity 的界面元素。请注意,在画中画模式下,您的 Activity 会在一个小窗口中显示。在画中画模式下,用户可能看不清小界面元素的详细信息,因此不会与这些界面元素互动。界面*简的视频播放 Activity 可提供出色的用户体验。Activity 应仅显示视频播放控件。在 Activity 进入画中画模式之前移除其他界面元素,并在 Activity 再次变为全屏时恢复这些元素:
@Override
public void onPictureInPictureModeChanged (boolean isInPictureInPictureMode, Configuration newConfig) {
if (isInPictureInPictureMode) {
// Hide the full-screen UI (controls, etc.) while in picture-in-picture mode.
} else {
// Restore the full-screen UI.

}
}
在画中画模式下继续播放视频
当您的 Activity 切换到画中画模式时,系统会将该 Activity 置于暂停状态并调用 Activity 的 onPause() 方法。如果该 Activity 在画中画模式下暂停,则视频播放不得暂停,而应继续播放。
在 Android 7.0 及更高版本中,当系统调用 Activity 的 onStop() 时,您应暂停视频播放;当系统调用 Activity 的 onStart() 时,您应恢复视频播放。这样一来,您就无需在 onPause() 中检查应用是否处于画中画模式,只需继续播放视频即可。如果您必须在 onPause() 实现中暂停播放,请通过调用 isInPictureInPictureMode() 检查画中画模式并相应地处理播放情况,例如:
@Override
public void onPause() {
// If called while in PIP mode, do not pause playback
if (isInPictureInPictureMode()) {
// Continue playback

} else {
// Use existing playback logic for paused Activity behavior.

}
}
切换视频/播放下一个时动态调画中画整宽高比例
/**
* 视频尺寸变化(上一个下一个时),动态调整PIP 宽高比
*
* @param with video宽度(非界面宽度)
* @param height video高度(非界面高度)
*/
private void videoSizeChange(int with, int height) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && (height != mVideoHeight || mVideoWith != with)) {
mVideoWith = with;
mVideoHeight = height;
if (mPictureInPictureParamsBuilder != null && mVideoWith != 0 && mVideoHeight != 0) {
//设置param宽高比,根据快高比例调整初始参数
Rational aspectRatio = new Rational(mVideoWith, mVideoHeight);
mPictureInPictureParamsBuilder.setAspectRatio(aspectRatio);

//设置更新PictureInPictureParams
setPictureInPictureParams(mPictureInPictureParamsBuilder.build());
}
}
}

进阶使用
添加自定义按钮:
方式一: 通过MediaSession达到如下图效果
(此处有关videoPlayer相关代码根据自己播放器灵活代入,仅供参考)

%title插图%num

当 Activity 进入画中画模式后,它默认没有获得输入焦点。要在画中画模式下接收输入事件,请使用 MediaSession.setCallback() 。如需详细了解如何使用 setCallback(),请参阅显示“ 正在播放 ”卡片。
首先在进入小窗前初始化MediaSessionCompat
private MediaSessionCompat mSession;
public static final long MEDIA_ACTIONS_PLAY_PAUSE = PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_PLAY_PAUSE;
public static final long MEDIA_ACTIONS_ALL = MEDIA_ACTIONS_PLAY_PAUSE | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;

private void initializeMediaSession() {
mSession = new MediaSessionCompat(this, TAG);
mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
mSession.setActive(true);
MediaControllerCompat.setMediaController(this, mSession.getController());

MediaMetadataCompat metadata = new MediaMetadataCompat.Builder().build();
mSession.setMetadata(metadata);

MediaSessionCallback mMediaSessionCallback = new MediaSessionCallback(videoPlayer);
mSession.setCallback(mMediaSessionCallback);

int state = videoPlayer.getCurrentState() == GSYVideoView.CURRENT_STATE_PLAYING ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
updatePlaybackState(state, MEDIA_ACTIONS_ALL, 0, 0);
}
在MediaSessionCompat.Callback中设置自己的播放器逻辑响应

private class MediaSessionCallback extends MediaSessionCompat.Callback {

private LocalListVideoPlayer movieView;
private int indexInPlaylist;

public MediaSessionCallback(LocalListVideoPlayer movieView) {
this.movieView = movieView;
indexInPlaylist = 1;
}

@Override
public void onPlay() {
super.onPlay();
movieView.getGSYVideoManager().start();
movieView.setIsInPictureInPictureMode(true);
movieView.setCurrentState(GSYVideoView.CURRENT_STATE_PLAYING);
updatePlaybackState(PlaybackStateCompat.STATE_PLAYING, 0, 0);
}

@Override
public void onPause() {
super.onPause();
movieView.getGSYVideoManager().pause();
movieView.setIsInPictureInPictureMode(true);
movieView.setCurrentState(GSYVideoView.CURRENT_STATE_PAUSE);
updatePlaybackState(PlaybackStateCompat.STATE_PAUSED, 0, 0);
}

@Override
public void onSkipToNext() {
super.onSkipToNext();
movieView.playNext();
}

@Override
public void onSkipToPrevious() {
super.onSkipToPrevious();
movieView.playLast();
}
}

//更新按钮操作
private void updatePlaybackState(@PlaybackStateCompat.State int state, int position, int mediaId) {
if (mSession.getController().getPlaybackState() != null) {
long actions = mSession.getController().getPlaybackState().getActions();
updatePlaybackState(state, actions, position, mediaId);
}
}

//初始化setPlaybackState
private void updatePlaybackState(@PlaybackStateCompat.State int state, long playbackActions, int position, int mediaId) {
if (mSession != null) {
PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder()
.setActions(playbackActions)
.setActiveQueueItemId(mediaId)
.setState(state, position, 1.0f);
mSession.setPlaybackState(builder.build());
}
}
在自己播放器状态更新时更新界面元素

@Override
public void onVideoStart() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if(isInPictureInPictureMode()) {
updatePlaybackState(PlaybackStateCompat.STATE_PLAYING, 0, 0);
}
}
}

@Override
public void onVideoPause() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if(isInPictureInPictureMode()) {
updatePlaybackState(PlaybackStateCompat.STATE_PAUSED, 0, 0);
}
}
}
方式2: 自定义按钮 (推荐)
(注意,按钮不超过三个,位置不可调节)
您还可以通过在进入画中画模式之前构建 PictureInPictureParams(使用 PictureInPictureParams.Builder. setActions())来明确指定自定义操作,并使用 enterPictureInPictureMode(android.app.PictureInPictureParams) 或 setPictureInPictureParams(android.app.PictureInPictureParams) 在进入画中画模式时传递这些参数。

首先自定义按钮初始化或刷新
private BroadcastReceiver mReceiver;
private static final String ACTION_MEDIA_CONTROL = “media_control”;
private static final String EXTRA_CONTROL_TYPE = “control_type”;
private static final int CONTROL_TYPE_PLAY = 1;
private static final int CONTROL_TYPE_PAUSE = 2;
private static final int CONTROL_TYPE_LAST = 3;
private static final int CONTROL_TYPE_NEXT = 4;
private static final int REQUEST_TYPE_PLAY = 1;
private static final int REQUEST_TYPE_PAUSE = 2;
private static final int REQUEST_TYPE_LAST = 3;
private static final int REQUEST_TYPE_NEXT = 4;

//进入画中画前判断状态,调用initPictureInPictureActions
private void initPictureInPictureActions() {
//int state = videoPlayer.getCurrentState() == GSYVideoView.CURRENT_STATE_PLAYING ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
//STATE_PLAYING = 3 ; STATE_PAUSED = 2
if (videoPlayer.getCurrentState() == GSYVideoView.CURRENT_STATE_PLAYING) {
updatePictureInPictureActions(R.drawable.gsy_play_video_icon_pause, “”, CONTROL_TYPE_PLAY, REQUEST_TYPE_PLAY);
} else {
updatePictureInPictureActions(R.drawable.gsy_play_video_icon_play, “”, CONTROL_TYPE_PAUSE, REQUEST_TYPE_PAUSE);
}
}
/**
* 刷新自定义按钮 (若是初始化,注意区分进入画中画前onpause状态)
*
* @param iconId
* @param title
* @param controlType
* @param requestCode 注意!! 每个intent的requestCode必须不一样
*/
void updatePictureInPictureActions(@DrawableRes int iconId, String title, int controlType, int requestCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (mPictureInPictureParamsBuilder == null) {
mPictureInPictureParamsBuilder = new PictureInPictureParams.Builder();
}
final ArrayList<RemoteAction> actions = new ArrayList<>();
// This is the PendingIntent that is invoked when a user clicks on the action item. You need to use distinct request codes for play and pause, or the PendingIntent won’t be updated.

//上一个
final PendingIntent intentLast = PendingIntent.getBroadcast(this, REQUEST_TYPE_NEXT, new Intent(ACTION_MEDIA_CONTROL).putExtra(EXTRA_CONTROL_TYPE, CONTROL_TYPE_LAST), 0);
actions.add(new RemoteAction(Icon.createWithResource(this, R.drawable.gsy_play_video_icon_last), “”, “”, intentLast));
//暂停/播放
final PendingIntent intentPause = PendingIntent.getBroadcast(this, requestCode, new Intent(ACTION_MEDIA_CONTROL).putExtra(EXTRA_CONTROL_TYPE, controlType), 0);
actions.add(new RemoteAction(Icon.createWithResource(this, iconId), title, title, intentPause));
//下一个
final PendingIntent intentNext = PendingIntent.getBroadcast(this, REQUEST_TYPE_LAST, new Intent(ACTION_MEDIA_CONTROL).putExtra(EXTRA_CONTROL_TYPE, CONTROL_TYPE_NEXT), 0);
actions.add(new RemoteAction(Icon.createWithResource(this, R.drawable.gsy_play_video_icon_next), “”, “”, intentNext));

mPictureInPictureParamsBuilder.setActions(actions);

// This is how you can update action items (or aspect ratio) for Picture-in-Picture mode. Note this call can happen even when the app is not in PiP mode.
setPictureInPictureParams(mPictureInPictureParamsBuilder.build());
}
}
响应按钮发出的intent
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
if (videoPlayer != null) {
isInPIPMode = isInPictureInPictureMode;
videoPlayer.setIsInPictureInPictureMode(isInPIPMode);
}
//自定义action形式
if (isInPictureInPictureMode) {
// Starts receiving events from action items in PiP mode.
mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null || !ACTION_MEDIA_CONTROL.equals(intent.getAction())) {
return;
}
// This is where we are called back from Picture-in-Picture action
final int controlType = intent.getIntExtra(EXTRA_CONTROL_TYPE, 0);
try {
switch (controlType) {
case CONTROL_TYPE_PLAY:
videoPlayer.getGSYVideoManager().start();
videoPlayer.setIsInPictureInPictureMode(true);
videoPlayer.setCurrentState(GSYVideoView.CURRENT_STATE_PLAYING);
break;
case CONTROL_TYPE_PAUSE:
videoPlayer.getGSYVideoManager().pause();
videoPlayer.setIsInPictureInPictureMode(true);
videoPlayer.setCurrentState(GSYVideoView.CURRENT_STATE_PAUSE);
break;
case CONTROL_TYPE_LAST:
videoPlayer.playLast();
break;
case CONTROL_TYPE_NEXT:
videoPlayer.playNext();
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
};
registerReceiver(mReceiver, new IntentFilter(ACTION_MEDIA_CONTROL));
} else {
// We are out of PiP mode. We can stop receiving events from it.
unregisterReceiver(mReceiver);
mReceiver = null;
}
}
当播放状态改变时更新按钮功能

videoPlayer.setLocalPlayerCallback(new LocalListVideoPlayer.LocalPlayerCallback() {
@Override
public void clickPIPMode() {
enterPiPMode();
}
@Override
public void OnPrepareVideoSizeChanged(int with, int height) {
videoSizeChange(with, height);
}
@Override
public void surfaceDestroyed() {
handleSurfaceDestroyed();
}
@Override
public void onVideoStart() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//自定义action刷新-开始播放-按钮替换为暂停
updatePictureInPictureActions(R.drawable.gsy_play_video_icon_pause, “”, CONTROL_TYPE_PLAY, REQUEST_TYPE_PLAY);
}
}
@Override
public void onVideoPause() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//自定义action刷新-暂停播放,按钮替换为开始
updatePictureInPictureActions(R.drawable.gsy_play_video_icon_play, “”, CONTROL_TYPE_PAUSE, REQUEST_TYPE_PAUSE);
}
关于浮窗关闭后仍有声音,无法获取浮窗关闭通知
方式一:监听SurfaceView-surfaceDestroyed()
在官方demo中,采用mediaSession方式, 以surfaceview的 surfaceDestroyed()回调关闭播放器
我采用的gsy播放器(同bilibili播放器),无法监听画中画浮窗关闭,采用如下的方法
在播放界面底层创建一个空的emptySurfaceView,通过callback获知浮窗被手动关闭 (此方法有个缺陷:在锁屏时也会回调此方法)
private SurfaceView emptySurfaceView;
….
emptySurfaceView = findViewById(R.id.emtpy_surface);
emptySurfaceView
.getHolder()
.addCallback(
new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if(mLocalPlayerCallback != null) {
mLocalPlayerCallback.surfaceDestroyed();
}
}
});
方式2:通过进入/退出/关闭画中画VideoActivity的生命周期判断(推荐)
操作画中画时VideoActivity相关生命周期梳理:

进入画中画–onPause

画中画返回全屏–OnResume

关闭画中画–onStop

全屏播放状态下下锁屏/解锁 onPause ,onStop / onStart,onResume

画中画状态下下锁屏/解锁 onStop / onStart

//是否支持pip画中画小窗模式(自行判断赋值时机)
protected boolean isSupportPipMode = false;
//是否已经在画中画模式(自行判断赋值时机)
public boolean isInPIPMode = false;
//是否点击进入过画中画模式–用于判断程序在后台时,由画中画返回全屏后退出,是否启动首页activity,以及onstop配合判断是否点击进入过画中画且在画中画模式
public boolean isEnteredPIPMode = false;

@Override
protected void onResume() {
super.onResume();
//画中画返回全屏会执行onresume
isEnteredPIPMode = false;
}

@Override
protected void onStop() {
super.onStop();
//备注: 在画中画模式下,onStop执行时, 若是关闭画中画,isInPictureInPictureMode()=false ; 若是锁屏,isInPictureInPictureMode()=true ; 判断锁屏isLockPage()一直为false
boolean inPictureInPictureMode = false;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
inPictureInPictureMode = isInPictureInPictureMode();
}
if (BuildConfig.DEBUG) {
Log.i(TAG, “onStop — inPictureInPictureMode=” + inPictureInPictureMode + ” ,isEnteredPIPMode=” + isEnteredPIPMode + ” ,isInPIPMode=” + isInPIPMode);
}
if (!inPictureInPictureMode && isInPIPMode && isEnteredPIPMode) {
//满足此条件下认为是关闭了画中画界面
if (BuildConfig.DEBUG) {
Log.w(TAG, “onStop — 判断为PIP下关闭画中画”);
}
handleSurfaceDestroyed();
return;
}
if (inPictureInPictureMode && isInPIPMode && isEnteredPIPMode && videoPlayer != null) {
//满足此条件下认为是画中画模式下锁屏
videoPlayer.onVideoPause();
isPause = true;
if (BuildConfig.DEBUG) {
Log.w(TAG, “onStop — 判断为PIP下锁屏”);
}
}
}
关于开启浮窗关闭后显示在*近任务列表
manifest添加 android:excludeFromRecents=”true”

参考:关于Android TaskAffinity的那些事儿

From Picture-in-Picture activity to Back-Stack activity not working in android?

关于APP进入后台,播放完成后吊起主页activity,主页activity也进入浮窗模式(部分机型偶现)
主页设置 android:supportsPictureInPicture=”false”无效

方案:采用遍历tasks,task.moveToFront() – task.moveToFront(); 避免采用startActivity方法使应用回到前台

参考:Launching Intent from notification opening in picture-in-picture window

public static void moveLauncherTaskToFront(Context context) {

ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
assert activityManager != null;
final List<ActivityManager.AppTask> appTasks = activityManager.getAppTasks();
for (ActivityManager.AppTask task : appTasks) {
final Intent baseIntent = task.getTaskInfo().baseIntent;
final Set<String> categories = baseIntent.getCategories();
if (categories != null && categories.contains(Intent.CATEGORY_LAUNCHER)) {
task.moveToFront();
return;
}
}
}
判断获取用户是否关闭了应用画中画模式

当您的应用处于画中画模式时,画中画窗口中的视频播放可能会对其他应用(例如,音乐播放器应用或语音搜索应用)造成音频干扰。为避免出现此问题,请在开始播放视频时请求音频焦点,并处理音频焦点更改通知,如管理音频焦点中所述。如果您在处于画中画模式时收到音频焦点丢失通知,请暂停或停止视频播放。

//音频焦点的监听
protected AudioManager mAudioManager;
mAudioManager = (AudioManager) getActivityContext().getApplicationContext().getSystemService(Context.AUDIO_SERVICE);

/**
* 监听是否有外部其他多媒体开始播放
*/
protected AudioManager.OnAudioFocusChangeListener onAudioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
@Override
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
//获得了Audio Focus
onGankAudio();
break;
case AudioManager.AUDIOFOCUS_LOSS:
//失去了Audio Focus,并将会持续很长的时间-暂停音频
onLossAudio();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
//暂时失去Audio Focus,并会很快再次获得
onLossTransientAudio();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
//暂时失去AudioFocus,但是可以继续播放,不过要在降低音量
onLossTransientCanDuck();
break;
}
}
};

问题处理记录:
页面切换进入/退出小窗播放状态处理
小窗关闭监听
10s退出
权限支持判断
自定义按钮状态
吊起主页页面状态错误
创建多个后台

参考:
app: MXplayer: (顶层activity: pipmenuActivity)
————————————————
版权声明:本文为CSDN博主「ZhangQiang-」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u011200604/article/details/104701266

300. *长上升子序列(JS实现)

300. *长上升子序列(JS实现)
1 题目
给定一个无序的整数数组,找到其中*长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: *长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
可能会有多种*长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
链接:https://leetcode-cn.com/problems/longest-increasing-subsequence
2 思路
这道题明显根据题意就知道应该用动态规划来做,注意题意,这里是子序列并不是要求连续的,设d[i]为0…i元素之间*大上升子序列长度,状态转移方程为 d[i+1] = max(d[0…i]) + 1
3代码
/**
 * @param {number[]} nums
 * @return {number}
 */
var lengthOfLIS = function(nums) {
    if (nums.length < 2) return nums.length;
    const d = [1];
    let maxAns = 1;
    for (let i=1; i<nums.length; i++) {
        let len = 0;
        for (let j=0;j<d.length;j++) {
            if (nums[i] > nums[j]) {
                len = Math.max(len, d[j]);
            }
        }
        d[i] = len + 1;
        maxAns = Math.max(maxAns, d[i])
    }
    return maxAns;
};

304. 二维区域和检索 – 矩阵不可变(JS实现)

304. 二维区域和检索 – 矩阵不可变(JS实现)
1 题目
给定一个二维矩阵,计算其子矩形范围内元素的总和,该子矩阵的左上角为 (row1, col1) ,右下角为 (row2, col2)。
上图子矩阵左上角 (row1, col1) = (2, 1) ,右下角(row2, col2) = (4, 3),该子矩形内元素的总和为 8。
示例:
给定 matrix = [
[3, 0, 1, 4, 2],
[5, 6, 3, 2, 1],
[1, 2, 0, 1, 5],
[4, 1, 0, 1, 7],
[1, 0, 3, 0, 5]
]
sumRegion(2, 1, 4, 3) -> 8
sumRegion(1, 1, 2, 2) -> 11
sumRegion(1, 2, 2, 4) -> 12
说明:
你可以假设矩阵不可变。
会多次调用 sumRegion 方法。
你可以假设 row1 ≤ row2 且 col1 ≤ col2。
链接:https://leetcode-cn.com/problems/range-sum-query-2d-immutable
2 思路
这道题可以用动态规划来做,首先sumRegion(2, 1, 4, 3)可以分割为sumRegion(0, 0, 4, 3) – sumRegion(0, 0, 4, 1) – sumRegion(0, 0, 2, 3) + sumRegion(0, 0, 2, 1),因此我只需计算d[i][j],d[i][j]为左上角为num[0][0],右下角为num[i][j]子矩阵的和。状态转移方程为d[i+1][j] = d[i][j] + sumRow(i+1, j), 其中 sumRow(i,j)为第i行0…j的和
3代码
/**
 * @param {number[][]} matrix
 */
var NumMatrix = function(matrix) {
    this.d = [];
    let rows = matrix.length;
    if (rows === 0) return;
    let cols = matrix[0].length;
    for (let i=0; i<rows; i++) {
        this.d[i] = [];
        let sumRow = 0;
        for (let j=0; j<cols; j++) {
            sumRow += matrix[i][j];
            this.d[i][j] = i > 0 ? this.d[i-1][j] + sumRow : sumRow;
        }
    }
};
/**
 * @param {number} row1
 * @param {number} col1
 * @param {number} row2
 * @param {number} col2
 * @return {number}
 */
NumMatrix.prototype.sumRegion = function(row1, col1, row2, col2) {
    let area = 0;
    if (this.d.length === 0) return area;
    if (col1 > 0) {
        area -= this.d[row2][col1-1];
    }
    if (row1 > 0) {
        area -= this.d[row1-1][col2];
    }
    if ( col1 > 0 && row1 > 0) {
        area += this.d[row1-1][col1-1];
    }
    return this.d[row2][col2] + area;
};
/**
 * Your NumMatrix object will be instantiated and called as such:
 * var obj = new NumMatrix(matrix)
 * var param_1 = obj.sumRegion(row1,col1,row2,col2)
 */
————————————————
版权声明:本文为CSDN博主「PAT-python-zjw」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zjw_python/article/details/108035266
1 题目
给定一个二维矩阵,计算其子矩形范围内元素的总和,该子矩阵的左上角为 (row1, col1) ,右下角为 (row2, col2)。
上图子矩阵左上角 (row1, col1) = (2, 1) ,右下角(row2, col2) = (4, 3),该子矩形内元素的总和为 8。
示例:
给定 matrix = [
[3, 0, 1, 4, 2],
[5, 6, 3, 2, 1],
[1, 2, 0, 1, 5],
[4, 1, 0, 1, 7],
[1, 0, 3, 0, 5]
]
sumRegion(2, 1, 4, 3) -> 8
sumRegion(1, 1, 2, 2) -> 11
sumRegion(1, 2, 2, 4) -> 12
说明:
你可以假设矩阵不可变。
会多次调用 sumRegion 方法。
你可以假设 row1 ≤ row2 且 col1 ≤ col2。
链接:https://leetcode-cn.com/problems/range-sum-query-2d-immutable
2 思路
这道题可以用动态规划来做,首先sumRegion(2, 1, 4, 3)可以分割为sumRegion(0, 0, 4, 3) – sumRegion(0, 0, 4, 1) – sumRegion(0, 0, 2, 3) + sumRegion(0, 0, 2, 1),因此我只需计算d[i][j],d[i][j]为左上角为num[0][0],右下角为num[i][j]子矩阵的和。状态转移方程为d[i+1][j] = d[i][j] + sumRow(i+1, j), 其中 sumRow(i,j)为第i行0…j的和
3代码
/**
 * @param {number[][]} matrix
 */
var NumMatrix = function(matrix) {
    this.d = [];
    let rows = matrix.length;
    if (rows === 0) return;
    let cols = matrix[0].length;
    for (let i=0; i<rows; i++) {
        this.d[i] = [];
        let sumRow = 0;
        for (let j=0; j<cols; j++) {
            sumRow += matrix[i][j];
            this.d[i][j] = i > 0 ? this.d[i-1][j] + sumRow : sumRow;
        }
    }
};
/**
 * @param {number} row1
 * @param {number} col1
 * @param {number} row2
 * @param {number} col2
 * @return {number}
 */
NumMatrix.prototype.sumRegion = function(row1, col1, row2, col2) {
    let area = 0;
    if (this.d.length === 0) return area;
    if (col1 > 0) {
        area -= this.d[row2][col1-1];
    }
    if (row1 > 0) {
        area -= this.d[row1-1][col2];
    }
    if ( col1 > 0 && row1 > 0) {
        area += this.d[row1-1][col1-1];
    }
    return this.d[row2][col2] + area;
};
/**
 * Your NumMatrix object will be instantiated and called as such:
 * var obj = new NumMatrix(matrix)
 * var param_1 = obj.sumRegion(row1,col1,row2,col2)
 */