简介

NineoldAndroids是Github上一个著名的动画库,简单来说,NineOldAndroids是一个向下兼容的动画库,主要是使低于API 11的系统也能够使用View的属性动画

网上已经有一些文章,介绍了这个库的设计,包括类结构和思想,例如
NineOldAnimations 源码解析
NineoldAndroids动画库源码分析
上面两篇文章都比较详细的介绍了NineoldAndroids的源码,可以说为大家看源码带来很大的方便。
那为什么我还要写这篇文章呢?
我们来看NineoldAndroids的类结构图:

因为NineoldAndroids的类结构比较复杂,即使单纯看上面两篇文章,也可能把人搞糊涂
本篇文章将剥离NineoldAndroids的具体细节,尝试只是显示其核心功能,也就是说写出一个简易的NineoldAndroids,并且在这个过程当中,了解Android实现动画的原理和思想
一理通百理明,与君共勉

开篇说明

1、本动画库以Int类型的属性值为例子,实现了Android库中ValueAnimator的功能,不了解ValueAnimator使用方式的朋友,可以参考这篇文章

2、大部分代码由NineoldAndroids中抽取,剥去一些不必要的实现细节,例如delayStart()方法等

3、ValueAnimator与ObjectAnimator有所区别,本库还没有实现ObjectAnimator。ObjectAnimator是继承自ValueAnimator,本质是通过ValueAnimator计算出的值,去更新View的属性。

现在我们先来看看该动画库的使用:

public class MainActivity extends Activity {TextView mTextView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mTextView = (TextView) findViewById(R.id.mtext);        //Value动画,设置目标值为3000CValueAnimator valueAnimator = CValueAnimator.ofInt(1000,2000,3000);//设置动画时间valueAnimator.setDuration(4000);valueAnimator.addUpdateListener(new CValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(CValueAnimator animation) {//将动画值,更新到textViewmTextView.setText(animation.getAnimatedValue() + "");mTextView.setTranslationY((Integer) animation.getAnimatedValue());mTextView.invalidate();}});//启动动画valueAnimator.start();}
}

使用方式和NineoldAndroids完全一样,也和Android原生的方式一样,对于使用过动画效果的朋友来说,应该非常简单。

类设计图

首先来看类设计图,这图相比原来的NineoldAndroids,做了很多精简,只是希望大家更加容易看懂NineoldAndroids的本质。

在进行下一步的分析之前,我们先来了解一下一些核心的类以及它们的作用。

  • CValueAnimator : 该类是 Animator 的子类,实现了动画的整个处理逻辑,也是最为核心的类;
  • TimeInterpolator : 时间插值器,它的作用是根据时间流逝的百分比来计算出当前属性值改变的百分比,系统预置的有 LinearInterpolator(线性插值器:匀速动画)、AccelerateDecelerateInterpolator(加速减速插值器:动画两头慢中间快)和 DecelerateInterpolator(减速插值器:动画越来越慢)等;
  • CTypeEvaluator : TypeEvaluator 的中文翻译为类型估值算法,它的作用是根据当前属性改变的百分比来计算改变后的属性值,系统预置的有 IntEvaluator(针对整型属性)、FloatEvaluator(针对浮点型属性);
  • CPropertyValuesHolder : PropertyValuesHolder 是持有目标属性 Property、setter 和 getter 方法、以及 KeyFrameSet 的类;
  • CKeyFrame : 一个 keyframe 对象由一对 time / value 的键值对组成,可以为动画定义某一特定时间的特定状态,Animator 传入的一个个参数映射为一个个 keyframe,存储相应的动画的触发时间和属性值;
  • CKeyFrameSet : 存储一个动画的关键帧集合;

动画流程解析

1、动画初始化

在调用start()方法之前,我们使用

CValueAnimator valueAnimator = CValueAnimator.ofInt(1000,2000,3000);

做了动画的初始化工作,那么我们具体做了上面呢?

1.1、关键帧

简单而言,就是把传入的属性值,例如例子中是1000,2000,3000,封装成关键帧对象CKeyFrame
所谓关键帧,就是在动画过程中一定要出现的帧。
我们知道,所谓动画也不可能是完全连续的,肯定会有一些间隔,只是间隔小于人眼视觉暂留时间,所以看起来就是连续的了。
所以从1000-2000这个过程,也不可能是完全连续的,也许是1000,1100,…1900,2000
其中一些帧就被丢失了,绝对不能丢失的帧,称为关键帧。
关键帧保留两个属性,一个是该帧所在的时间(其实是一个百分比),一个是帧值

public abstract class CKeyframe implements Cloneable {/*** 时间*/float mFraction;/*** 属性值类型*/Class mValueType;/*** 插值器*/private /*Time*/Interpolator mInterpolator = null;public static CKeyframe ofInt(float fraction, int value) {return new IntCKeyframe(fraction, value);}public static CKeyframe ofInt(float fraction) {return new IntCKeyframe(fraction);}public abstract Object getValue();/*** INT类型值得关键帧*/public static class IntCKeyframe extends CKeyframe {/*** 关键帧的值*/int mValue;IntCKeyframe(float fraction, int value) {mFraction = fraction;mValue = value;mValueType = int.class;}IntCKeyframe(float fraction){mFraction = fraction;mValueType = int.class;         }public int getIntValue() {return mValue;}public void setValue(Object value) {if (value != null && value.getClass() == Integer.class) {mValue = ((Integer)value).intValue();}}@Overridepublic Object getValue() {return mValue;}}public float getFraction() {return mFraction;}public /*Time*/Interpolator getInterpolator() {return mInterpolator;}
}

1.2、关键帧集合

我们生成关键帧对象以后,将关键帧存入一个集合,称为关键帧集合,也就是CKeyFrameSet类。
CKeyFrameSet类中有一个CTypeEvaluator成员对象,这对象可以通过当前动画进行的百分比,计算出两个关键帧之间的值

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

可以看到,IntCEvaluator其实就是一个线性计算fraction是百分比startValue是起始值endValue是目标值,不同的fraction会产生不同的结果。
显然对于两个关键帧来说,前一个关键帧的值,就是起始值,后一个关键帧的值,就是目标值

在CKeyFrameSet中是这样调用这个方法:

public int getIntValue(float fraction) {if (mNumKeyframes == 2) {//只有两个关键帧的情况if (firstTime) {firstTime = false;firstValue = ((CKeyframe.IntCKeyframe) mKeyframes.get(0)).getIntValue();lastValue = ((CKeyframe.IntCKeyframe) mKeyframes.get(1)).getIntValue();deltaValue = lastValue - firstValue;}if (mInterpolator != null) {fraction = mInterpolator.getInterpolation(fraction);}if (mEvaluator == null) {return firstValue + (int)(fraction * deltaValue);} else {return ((Number)mEvaluator.evaluate(fraction, firstValue, lastValue)).intValue();}}....CKeyframe.IntCKeyframe prevKeyframe = (CKeyframe.IntCKeyframe) mKeyframes.get(0);for (int i = 1; i < mNumKeyframes; ++i) {//多个关键帧CKeyframe.IntCKeyframe nextKeyframe = (CKeyframe.IntCKeyframe) mKeyframes.get(i);if (fraction < nextKeyframe.getFraction()) {final /*Time*/Interpolator interpolator = nextKeyframe.getInterpolator();if (interpolator != null) {fraction = interpolator.getInterpolation(fraction);}float intervalFraction = (fraction - prevKeyframe.getFraction()) /(nextKeyframe.getFraction() - prevKeyframe.getFraction());int prevValue = prevKeyframe.getIntValue();int nextValue = nextKeyframe.getIntValue();return mEvaluator == null ?prevValue + (int)(intervalFraction * (nextValue - prevValue)) :((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).intValue();}prevKeyframe = nextKeyframe;}// shouldn't get herereturn ((Number)mKeyframes.get(mNumKeyframes - 1).getValue()).intValue();}

方法有点长,大家可以看只有两个关键帧的情况是怎么计算的,就比较简单,其实就是线性计算。

1.3、非线性方法

问题是,难道每次我们都希望1000-2000直接是线性增长的吗?如果我希望先快后慢呢?
了解插值器的朋友,应该就明白,这个功能我们可以通过Interpolator去做,但是Interpolator改变的是fraction的增长速度,也就是加速度(勉强可以这样理解)。
从而实现非线性效果,所以显然CKeyframeSet要持有一个Interpolator对象

1.4、CPropertyValuesHolder类,持有属性名称和CKeyFrameSet

顾名思义,CPropertyValuesHolder就是持有属性和值得一个类。
CPropertyValuesHolder是动画库中的一个核心类,但是在本简易库削减了其功能,因为我们只需要实现值得变化,没有针对具体的属性,例如scale,rotate等,所以不需要提供View属性修改的方法。
其实这个类,是为ObjectAnimator做了比较大的准备,但是本篇文章不涉及。
我们关注的是,这个类持有CKeyFrameSet。

public class CPropertyValuesHolder implements Cloneable {/*** 属性名称*/String mPropertyName;/*** 关键帧集合*/CKeyframeSet mKeyframeSet = null;private static final CTypeEvaluator sIntEvaluator = new IntCEvaluator();private static final CTypeEvaluator sFloatEvaluator = new FloatCEvaluator();private CTypeEvaluator mEvaluator = sIntEvaluator;/*** 属性值*/private Object mAnimatedValue;/*** 属性值类型 */Class mValueType;private CPropertyValuesHolder(String propertyName) {mPropertyName = propertyName;}/*** 返回一个属性值类型为int的CPropertyValuesHolder* @param propertyName* @param values* @return*/public static CPropertyValuesHolder ofInt(String propertyName, int... values) {return new IntCPropertyValuesHolder(propertyName, values);}/*** 属性值类型为int的CPropertyValuesHolder*/static class IntCPropertyValuesHolder extends CPropertyValuesHolder {IntCKeyframeSet mIntKeyframeSet;public IntCPropertyValuesHolder(String propertyName, int... values) {super(propertyName);setIntValues(values);}@Overridepublic void setIntValues(int... values) {super.setIntValues(values);mIntKeyframeSet = (IntCKeyframeSet) mKeyframeSet;}}/*** 返回当前属性值* @return*/Object getAnimatedValue() {return mAnimatedValue;}/*** 设置属性值类型,这里具体到int类型* 对于每个Int类型值,例如100,200,1000* 生成一个CKeyFrame关键帧对象,并且将这些对象包装成一个set集合* @param values*/public void setIntValues(int... values) {mValueType = int.class;mKeyframeSet = CKeyframeSet.ofInt(values);}void init() {if (mEvaluator == null) {// We already handle int and float automatically, but not their Object// equivalentsmEvaluator = (mValueType == Integer.class) ? sIntEvaluator :(mValueType == Float.class) ? sFloatEvaluator :null;}if (mEvaluator != null) {// KeyframeSet knows how to evaluate the common types - only give it a custom// evaluator if one has been set on this classmKeyframeSet.setEvaluator(mEvaluator);}}/*** 让CKeyframeSet通过关键帧计算属性值* @param fraction*/void calculateValue(float fraction) {mAnimatedValue = mKeyframeSet.getValue(fraction);}
}

1.5、CValueAnimator.ofInt(1000,2000,3000);到底做了什么?

    /*** CPropertyValuesHolder是一个包装类* 可以看做是,需要动画的属性或者值的对象实例*/CPropertyValuesHolder[] mValues;/*** 属性值类型为int的动画* @param values* @return*/public static CValueAnimator ofInt(int... values) {CValueAnimator anim = new CValueAnimator();anim.setIntValues(values);return anim;}/*** 根据一系列属性值,生成CPropertyValuesHolder* CPropertyValuesHolder这个类的意义就是,持有某个属性的一系列值,* 例如"scale(缩放属性)",其若干个值为100,200,1000等。* 也就是说在规定时间内,"scale"的值会从100增长到1000* @param values*/public void setIntValues(int... values) {if (values == null || values.length == 0) {return;}if (mValues == null || mValues.length == 0) {//属性名称为"",说明只是数值改变,和具体属性无关setValues(new CPropertyValuesHolder[]{CPropertyValuesHolder.ofInt("", values)});} else {CPropertyValuesHolder valuesHolder = mValues[0];valuesHolder.setIntValues(values);}// New property/values/target should cause re-initialization prior to startingmInitialized = false;}public void setValues(CPropertyValuesHolder... values) {mValues = values;mInitialized = false;}

原理就是根据1000,2000,3000,生成了一个CPropertyValuesHolder对象,并且将它保存了起来。

2、调用start(),开始动画!

目前万事俱备,只等调用start()方法开始动画了。
直接来看简化过后的start()方法

public void  start() {if (Looper.myLooper() == null) {//当前线程必须调用了Looper.loop()方法throw new AndroidRuntimeException("Animators may only be run on Looper threads");}mPlayingState = STOPPED;sPendingAnimations.get().add(this);/*** 初始化动画时间,也就是设置起始运行时间为0* 并且计算起始属性值值*/setCurrentPlayTime(getCurrentPlayTime());mPlayingState = STOPPED;mRunning = true;//通知监听器,动画开始if (mListeners != null) {ArrayList<AnimatorListener> tmpListeners =(ArrayList<AnimatorListener>) mListeners.clone();int numListeners = tmpListeners.size();for (int i = 0; i < numListeners; ++i) {tmpListeners.get(i).onAnimationStart(this);}}//Handler,通过自己给自己发送消息,实现不断进行动画AnimationHandler animationHandler = sAnimationHandler.get();if (animationHandler == null) {animationHandler = new AnimationHandler();sAnimationHandler.set(animationHandler);}animationHandler.sendEmptyMessage(ANIMATION_START);}

动画过程我们可以这样想:
1、获取当前时间为startTime,即动画起始时间,并且初始化动画状态,例如1000,2000,3000,那么setCurrentPlayTime()方法的其中一个工作就是初始化状态为1000
2、通知动画开始监听器,动画开始
3、使用AnimationHandler实现循环,首先给AnimationHandler发送了一条ANIMATION_START信息

显然,主要工作就是在AnimationHandler里面进行的

/*** 该handler用于处理两个消息* ANIMATION_START也就是动画开始* ANIMATION_FRAME也就是运行某一帧*/private static class AnimationHandler extends Handler {        @Overridepublic void handleMessage(Message msg) {boolean callAgain = true;//当前运行动画队列ArrayList<CValueAnimator> animations = sAnimations.get();switch (msg.what) {case ANIMATION_START://当前等候动画队列ArrayList<CValueAnimator> pendingAnimations = sPendingAnimations.get();if (animations.size() > 0) {callAgain = false;}while (pendingAnimations.size() > 0) {//如果等候的动画大于0ArrayList<CValueAnimator> pendingCopy =(ArrayList<CValueAnimator>) pendingAnimations.clone();pendingAnimations.clear();int count = pendingCopy.size();for (int i = 0; i < count; ++i) {CValueAnimator anim = pendingCopy.get(i);// If the animation has a startDelay, place it on the delayed listanim.startAnimation();//启动这些动画}}case ANIMATION_FRAME://当前时间long currentTime = AnimationUtils.currentAnimationTimeMillis();//当前已经结束的动画队列ArrayList<CValueAnimator> endingAnims = sEndingAnims.get();//正在运行的对话数量int numAnims = animations.size();int i = 0;while (i < numAnims) {CValueAnimator anim = animations.get(i);if (anim.animationFrame(currentTime)) {//更新每个运行动画的数值,如果已经结束,加入endingAnims对象endingAnims.add(anim);}if (animations.size() == numAnims) {++i;} else {//在动画运行过程中,可能有些动画被取消--numAnims;endingAnims.remove(anim);}}if (endingAnims.size() > 0) {for (i = 0; i < endingAnims.size(); ++i) {endingAnims.get(i).endAnimation();}endingAnims.clear();}// If there are still active or delayed animations, call the handler again// after the frameDelay//如果还有活动的动画,在默认每帧间隔时间以后,再次调用,更新属性值if (callAgain && (!animations.isEmpty())) {sendEmptyMessageDelayed(ANIMATION_FRAME, Math.max(0, sFrameDelay -(AnimationUtils.currentAnimationTimeMillis() - currentTime)));}break;}}}

这个类做了这些工作:

1、ANIMATION_START状态:

其实就是调用了等待队列pendingAnimations中CValueAnimator对象的startAnimation()方法

/*** 启动动画*/private void startAnimation() {//初始化动画initAnimation();//将等候队列中的动画,加入运行对象sAnimations.get().add(this);}

在该方法中,初始化了动画(其实这里调用initAnimation()没有实际作用,因为之前已经初始化过了);
然后就是将动画放入运行队列。

2、ANIMATION_FRAME状态:

我们注意到ANIMATION_START状态以后,并没有使用break,所以会接着执行ANIMATION_FRAME
对每个运行动画,调用其了animationFrame()方法

/*** 根据当前时间,计算运行百分比,然后调用animateValue更新当前属性值* @param currentTime* @return*/boolean animationFrame(long currentTime) {boolean done = false;if (mPlayingState == STOPPED) {mPlayingState = RUNNING;if (mSeekTime < 0) {mStartTime = currentTime;} else {mStartTime = currentTime - mSeekTime;// Now that we're playing, reset the seek timemSeekTime = -1;}}switch (mPlayingState) {case RUNNING:case SEEKED:float fraction = mDuration > 0 ? (float)(currentTime - mStartTime) / mDuration : 1f;if (fraction >= 1f) {//百分比大于1,结束动画done = true;fraction = Math.min(fraction, 1.0f);}animateValue(fraction);break;}return done;}

这个方法会根据当前时间,判断动画是否已经结束,如果是,返回true,这些动画就会进入sEndingAnims队列,做最后的结束通知工作
否则,其实就是调用了自己的CPropertyValuesHolder计算当前属性值

/*** 根据百分比,更新属性值* @param fraction*/void animateValue(float fraction) {fraction = mInterpolator.getInterpolation(fraction);mCurrentFraction = fraction;int numValues = mValues.length;for (int i = 0; i < numValues; ++i) {mValues[i].calculateValue(fraction);//让CPropertyValuesHolder计算属性值}//通知监听器,属性值更新if (mUpdateListeners != null) {int numListeners = mUpdateListeners.size();for (int i = 0; i < numListeners; ++i) {mUpdateListeners.get(i).onAnimationUpdate(this);}}}

3、新的属性值计算结束

走完两个case,新的属性值就计算出来了,我们通过getAnimatedValue()就可以拿到

/*** 获取当前属性值* @return*/public Object getAnimatedValue() {if (mValues != null && mValues.length > 0) {return mValues[0].getAnimatedValue();}// Shouldn't get here; should always have values unless ValueAnimator was set up wrongreturn null;}

那么怎么让动画继续计算下一个属性值呢?
注意最后

// If there are still active or delayed animations, call the handler again// after the frameDelay//如果还有活动的动画,在默认每帧间隔时间以后,再次调用,更新属性值if (callAgain && (!animations.isEmpty())) {sendEmptyMessageDelayed(ANIMATION_FRAME, Math.max(0, sFrameDelay -(AnimationUtils.currentAnimationTimeMillis() - currentTime)));}

也就是在规定的帧间隔以后,AnimationHandler给自己再次发送一个ANIMATION_FRAME消息,进行下一次属性值的计算
最后,如上面所说,animationFrame()返回true,才真正结束动画

写在最后

到此为止,我要介绍的内容就说完了。
大家只要了解关键帧CKeyframe,CPropertyValuesHolder,估值算法CTypeEvaluator,以及使用handler循环计算动画值的原理
就应该可以了解到NineoldAndroids动画库设计的核心。
文章以及将NineoldAndroids的代码做了很多精简,就是希望大家可以明白本质,如果还有疑问的地方,可以下载我的测试代码,运行感受一下下。
也可以给我留言,一起探讨。
源码下载地址(manimation包是我精简过的代码,animation,util,view三个包中的,是NineoldAndroids的代码,大家可以对比看看)。

打造简易NineoldAndroids动画库,深入理解Android动画原理相关推荐

  1. NineoldAndroids动画库源码分析

    简介 做Android开发的同学很多都知道或者使用过一个动画库,那就是NineOldAndroids,它的作者及其牛X,好几个著名的开源库都是他的作品,具体大家可以看他的JakeWharton.简单来 ...

  2. html中如何设置动画鼠标,使用animate动画库添加鼠标经过动画

    使用animate动画库添加鼠标经过动画 蓝叶    网站设计    2016-11-27    11679    4评论 animate是一个css3动画库,使用它简单几步就能很轻松的为网站增加各种 ...

  3. Android之开源框架NineOldAndroids动画库

    1.介绍 Android3.0推出了全新的AnimationAPI,使用起来很方便,但是不能在3.0以下版本使用,NineOldAndroids是一个可以在任意Android版本上使用的Animati ...

  4. react-native系列(13)动画篇:Animated动画库和LayoutAnimation布局动画详解

    动画概念了解 流畅.有意义的动画对于APP户体验来说是非常重要的,用RN开发实现动画有三种方法: requestAnimationFrame:称为帧动画,原理是通过同步浏览器的刷新频率不断重新渲染界面 ...

  5. 前端之vue3使用动画库animate.css(含动画、过渡)

    动画与过渡 一.动画效果 1.默认动画 实例 动画语法 2.给transition指定name 二.过渡效果 三.多个元素过渡 四.vue3使用动画库 动画库animate.css √ 五.总结 一. ...

  6. android安装教程!深入理解Flutter动画原理,大厂面试题汇总

    背景 知乎客户端中有一个自己维护的 Hybrid 框架,在此基础上开发了一些 Hybrid 页面,当需要前端或者客户端开发接口的时候,就涉及到联调的问题. 和一般的 前端 <=> 服务端, ...

  7. Android 动画原理

    简介 Android 平台提供了三类动画,1.Tween动画,就是对场景里的对象不断的进行图像变化来产生动画效果(旋转.平移.放缩和渐变):2. Frame动画,即顺序的播放事先做好的图像,与gif图 ...

  8. android 动画原理二

    简介: 这是由两部分组成的 Android 动画框架详解的第二部分实例篇.在阅读本篇之前,建议您首先阅读本系列的第一部分 Android 动画框架详解之原理篇.原理篇详细介绍了 Android 动画框 ...

  9. 深入理解Vue动画原理

    深入讲解 Vue 动画原理 文档 过渡 & 动画 轮播组件slides 轮播难点在于最末位到首位的切换方式,在讲轮播之前需要讲下动画. Vue动画支持很多种不同的方式. Vue动画方式1 - ...

最新文章

  1. 我竟然混进了Python高级圈子!
  2. jenkins maven没有使用全局设置文件地址_持续集成工具Jenkins看这篇就够啦!
  3. Scrapy入门教程
  4. 【校招面试 之 C/C++】第17题 C 中的malloc相关
  5. Python超强全方位学习路线分享(附视频+书籍+面试链接)
  6. em算法示例_带有示例HTML'em'标签
  7. 简书python_python爬虫(以简书为例)
  8. JMeter压力测试步骤
  9. Qt制作音乐播放器按钮
  10. QComboBox 仅在展开时显示图标
  11. [UE4][C++]简单超人小游戏(游戏接受键盘事件)
  12. Logistic-tent混沌系统matlab
  13. 如何看linux是ubuntu还是centos
  14. 思岚激光建图传感器slamtec Mapper使用便捷性测评
  15. j2ee常用工作流比较(shart、osworkflow、jbpm)
  16. Spring 依赖注入的理解及三种注入方式
  17. 2021款途锐噪音测试软件,试驾2021款大众途锐:这才是原汁原味的德国沃尔夫斯堡的味道...
  18. 【无标题】移动端App下载页面模版
  19. linux的时区设置函数tzset()
  20. 元宵节营销活动策划,轻松拿下用户

热门文章

  1. 齐博php百度编辑器上传图片_齐博CMS整合百度编辑器上传附件的BUG以及解决办法...
  2. opencv+python图像识别,麻将牌识别,实现自动打牌方案
  3. java判断麻将听牌_和牌看听:麻将听牌种类大全
  4. 面试题:为什么ConcurrentHashMap的读操作不需要加锁?
  5. 排查Java宕机,weblogic宕机问题排查
  6. 《Patterns, Principles, and Pract》— chapter15 Value Objects
  7. 淘金(bzoj 3131)
  8. 特色英文短语[转帖]
  9. 多媒体计算机维修记载,多媒体个人工作总结
  10. 街霸 彩虹 m7 android,街霸2四大天王M7