首页 > 代码库 > [Material Design] MaterialButton 效果进阶 动画自动移动进行对齐效果

[Material Design] MaterialButton 效果进阶 动画自动移动进行对齐效果

做 Android 动画效果一段时间了,感觉深深喜欢上了钻研特效。在手机上显示自己的特效是一件很不错的事情。

废话不多说,前面几天我发布了:[Material Design] 教你做一个Material风格、动画的按钮(MaterialButton)

在其中我讲解了我对 Android L 中 Material 效果的按钮的动画实现方式,今天的文章将基于其上进行进阶讲解新的特效。

在 MaterialButton 中的特效原理是:用户点击时启动一个动画,该动画是在点击位置画颜色渐变同时半径变大的圆,从而实现扩散效果;具体可点击上面的链接查看一下。在按钮中的这样的特效距离谷歌的还是有很大的差距的,下面来对比一下:

官方的:


我们上个版本的:


可以看出谷歌的是有位移效果,而我们的是原地扩散的效果,当然动画速度这个与PS的设置有关,不做比较,实际速度比上面的略快。

下面咱们就来试试做做位移的特效,先画个图给大家看看:


相信大家都能看懂,第一种就是之前的实现方式,只是在原地扩散,第二种就是新的,将在扩散的同时向中心靠拢,且为了达到更加好的视觉效果,靠拢中心的XY轴速度并不是一样的,X轴的靠拢时间=整个扩散时间,向Y轴靠拢的时间~=整个扩散时间*0.3(且都是先快后慢),现在来看看成品效果:


点击中间的时候与第一种差距不大,但是点击两边的时候将会有明显的差距,能感觉到向中心靠拢的触觉。是不是和谷歌的相比起来又近了一些了?


说了这个多的理论与演示,下面来说说整个的实现:

首先我们抽取上一篇文章的成果作为这篇的开头,具体怎么新建控件就不再做介绍了,先看看上一篇的代码成果(该代码进行了一定的修改):

 

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public class MaterialButton extends Button {  
  2.     private static final Interpolator ANIMATION_INTERPOLATOR = new DecelerateInterpolator();  
  3.     private static final long ANIMATION_TIME = 600;  
  4.   
  5.     private Paint backgroundPaint;  
  6.     private static ArgbEvaluator argbEvaluator = new ArgbEvaluator();  
  7.     private float paintX, paintY, radius;  
  8.   
  9.   
  10.     public MaterialButton(Context context) {  
  11.         super(context);  
  12.         init(null, 0);  
  13.     }  
  14.   
  15.     public MaterialButton(Context context, AttributeSet attrs) {  
  16.         super(context, attrs);  
  17.         init(attrs, 0);  
  18.     }  
  19.   
  20.     public MaterialButton(Context context, AttributeSet attrs, int defStyle) {  
  21.         super(context, attrs, defStyle);  
  22.         init(attrs, defStyle);  
  23.     }  
  24.   
  25.     @SuppressWarnings("deprecation")  
  26.     private void init(AttributeSet attrs, int defStyle) {  
  27.         ...  
  28.     }  
  29.   
  30.   
  31.     @SuppressWarnings("NullableProblems")  
  32.     @Override  
  33.     protected void onDraw(Canvas canvas) {  
  34.         canvas.save();  
  35.         canvas.drawCircle(paintX, paintY, radius, backgroundPaint);  
  36.         canvas.restore();  
  37.   
  38.         super.onDraw(canvas);  
  39.     }  
  40.   
  41.     @SuppressWarnings("NullableProblems")  
  42.     @Override  
  43.     public boolean onTouchEvent(MotionEvent event) {  
  44.         if (event.getAction() == MotionEvent.ACTION_DOWN) {  
  45.             paintX = event.getX();  
  46.             paintY = event.getY();  
  47.             startRoundAnimator();  
  48.         }  
  49.         return super.onTouchEvent(event);  
  50.     }  
  51.   
  52.     /** 
  53.      * ============================================================================================= 
  54.      * The Animator methods 
  55.      * ============================================================================================= 
  56.      */  
  57.   
  58.     /** 
  59.      * Start Round Animator 
  60.      */  
  61.     private void startRoundAnimator() {  
  62.         float start, end, height, width;  
  63.         long time = (long) (ANIMATION_TIME * 1.85);  
  64.   
  65.         //Height Width  
  66.         height = getHeight();  
  67.         width = getWidth();  
  68.   
  69.         //Start End  
  70.         if (height < width) {  
  71.             start = height;  
  72.             end = width;  
  73.         } else {  
  74.             start = width;  
  75.             end = height;  
  76.         }  
  77.   
  78.         float startRadius = (start / 2 > paintY ? start - paintY : paintY) * 1.15f;  
  79.         float endRadius = (end / 2 > paintX ? end - paintX : paintX) * 0.85f;  
  80.   
  81.         //If The approximate square approximate square  
  82.         if (startRadius > endRadius) {  
  83.             startRadius = endRadius * 0.6f;  
  84.             endRadius = endRadius / 0.8f;  
  85.             time = (long) (time * 0.5);  
  86.         }  
  87.   
  88.         AnimatorSet set = new AnimatorSet();  
  89.         set.playTogether(  
  90.                 ObjectAnimator.ofFloat(this, mRadiusProperty, startRadius, endRadius),  
  91.                 ObjectAnimator.ofObject(this, mBackgroundColorProperty, argbEvaluator, attributes.getColor(1), attributes.getColor(2))  
  92.         );  
  93.         // set Time  
  94.         set.setDuration((long) (time / end * endRadius));  
  95.         set.setInterpolator(ANIMATION_INTERPOLATOR);  
  96.         set.start();  
  97.     }      
  98.   
  99.   
  100.     /** 
  101.      * ============================================================================================= 
  102.      * The custom properties 
  103.      * ============================================================================================= 
  104.      */  
  105.   
  106.       
  107.     private Property<MaterialButton, Float> mRadiusProperty = new Property<MaterialButton, Float>(Float.class, "radius") {  
  108.         @Override  
  109.         public Float get(MaterialButton object) {  
  110.             return object.radius;  
  111.         }  
  112.   
  113.         @Override  
  114.         public void set(MaterialButton object, Float value) {  
  115.             object.radius = value;  
  116.             invalidate();  
  117.         }  
  118.     };  
  119.   
  120.     private Property<MaterialButton, Integer> mBackgroundColorProperty = new Property<MaterialButton, Integer>(Integer.class, "bg_color") {  
  121.         @Override  
  122.         public Integer get(MaterialButton object) {  
  123.             return object.backgroundPaint.getColor();  
  124.         }  
  125.   
  126.         @Override  
  127.         public void set(MaterialButton object, Integer value) {  
  128.             object.backgroundPaint.setColor(value);  
  129.         }  
  130.     };  
  131.   
  132. }  

在上述代码中我们实现了点击时进行扩散的效果,初始化控件部分由于我加入了许多的代码这里删除了,具体可以看看我的项目实现,最后会给出地址。

现在基于此开工!

首先我们建立 两个新的属性 分别X坐标与Y坐标属性:

 

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. private Property<MaterialButton, Float> mPaintXProperty = new Property<MaterialButton, Float>(Float.class, "paintX") {  
  2.         @Override  
  3.         public Float get(MaterialButton object) {  
  4.             return object.paintX;  
  5.         }  
  6.   
  7.         @Override  
  8.         public void set(MaterialButton object, Float value) {  
  9.             object.paintX = value;  
  10.         }  
  11.     };  
  12.   
  13.     private Property<MaterialButton, Float> mPaintYProperty = new Property<MaterialButton, Float>(Float.class, "paintY") {  
  14.         @Override  
  15.         public Float get(MaterialButton object) {  
  16.             return object.paintY;  
  17.         }  
  18.   
  19.         @Override  
  20.         public void set(MaterialButton object, Float value) {  
  21.             object.paintY = value;  
  22.         }  
  23.     };  

在这两个属性中并未调用第一篇所说的 “ invalidate();”方法进行界面刷新,因为该方法应该放在持续时间最长的半径属性中调用。

之后我们获取到高宽 以及根据高和宽 计算出对应的 开始半径与结束半径:

 

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. <span style="white-space:pre">    </span>float start, end, height, width, speed = 0.3f;  
  2.         long time = ANIMATION_TIME;  
  3.   
  4.         //Height Width  
  5.         height = getHeight();  
  6.         width = getWidth();  
  7.   
  8.         //Start End  
  9.         if (height < width) {  
  10.             start = height;  
  11.             end = width;  
  12.         } else {  
  13.             start = width;  
  14.             end = height;  
  15.         }  
  16.         start = start / 2 > paintY ? start - paintY : paintY;  
  17.         end = end * 0.8f / 2f;  
  18.   
  19.         //If The approximate square approximate square  
  20.         if (start > end) {  
  21.             start = end * 0.6f;  
  22.             end = end / 0.8f;  
  23.             time = (long) (time * 0.65);  
  24.             speed = 1f;  
  25.         }  

我们首先比较了高与宽的长度 把短的赋予为开始半径 长的赋予为结束半径。

第二步,我们把开始长度除以2  得出其一半的长度 然后与 点击时的Y轴坐标比较,如果Y轴较长则取Y,如果不够则取其相减结果。这样能保证点击开始时的半径能刚好大于其高或者宽(短的一边),这样就不会出现小圆扩散的效果,看起来将会由椭圆的效果(当然以后将会直接画出椭圆)

第三步,我们运算出结束半径,同时保证结束半径为长的一边的一半的8/10 这样的效果是不会出现布满整个控件的情况。8/10 的空间刚好是个不错的选择。

第四步,判断开始长度是否大于结束长度,如果是(近似正方形情况),进行一定规则的重新运算,保证其开始半径能刚好与控件长度差不多(0.48左右),结束半径能刚刚布满控件,同时减少动画时间

当然,我现在才发现了一个BUG,在第二步的地方的BUG,大家看看,希望能提出是哪里的BUG;就当是一个互动!该BUG将会在下个版本修复。


之后我们建立每个属性的动画,并给每个属性动画设置对应的时间:

 

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. <span style="white-space:pre">    </span>//PaintX  
  2.         ObjectAnimator aPaintX = ObjectAnimator.ofFloat(this, mPaintXProperty, paintX, width / 2);  
  3.         aPaintX.setDuration(time);  
  4.         //PaintY  
  5.         ObjectAnimator aPaintY = ObjectAnimator.ofFloat(this, mPaintYProperty, paintY, height / 2);  
  6.         aPaintY.setDuration((long) (time * speed));  
  7.         //Radius  
  8.         ObjectAnimator aRadius = ObjectAnimator.ofFloat(this, mRadiusProperty, start, end);  
  9.         aRadius.setDuration(time);  
  10.         //Background  
  11.         ObjectAnimator aBackground = ObjectAnimator.ofObject(this, mBackgroundColorProperty, argbEvaluator, attributes.getColor(1), attributes.getColor(2));  
  12.         aBackground.setDuration(time);  


可以看见Y轴的时间乘以了一个speed变量,该变量默认是0.3 如果是近似正方形将初始化为1以便能同时对齐到中心位置,在上一步中有对应变量。

然后咱们把所有的属性动画添加到一个动画集并设置其速度方式为:先快后慢。最后启动该动画集。

 

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. //AnimatorSet  
  2. AnimatorSet set = new AnimatorSet();  
  3. set.playTogether(aPaintX, aPaintY, aRadius, aBackground);  
  4. set.setInterpolator(ANIMATION_INTERPOLATOR);  
  5. set.start();  


以上就是最新的动画效果的实现原理及代码了,当然我们可以将其合并到第一篇的代码中,并使用一个 Bool 属性来控制使用哪一种动画:

 

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public class MaterialButton extends Button {  
  2.     private static final Interpolator ANIMATION_INTERPOLATOR = new DecelerateInterpolator();  
  3.     private static final long ANIMATION_TIME = 600;  
  4.   
  5.     private Paint backgroundPaint;  
  6.     private static ArgbEvaluator argbEvaluator = new ArgbEvaluator();  
  7.     private float paintX, paintY, radius;  
  8.     private Attributes attributes;  
  9.   
  10.     public MaterialButton(Context context) {  
  11.         super(context);  
  12.         init(null, 0);  
  13.     }  
  14.   
  15.     public MaterialButton(Context context, AttributeSet attrs) {  
  16.         super(context, attrs);  
  17.         init(attrs, 0);  
  18.     }  
  19.   
  20.     public MaterialButton(Context context, AttributeSet attrs, int defStyle) {  
  21.         super(context, attrs, defStyle);  
  22.         init(attrs, defStyle);  
  23.     }  
  24.   
  25.     @SuppressWarnings("deprecation")  
  26.     private void init(AttributeSet attrs, int defStyle) {  
  27.         ...  
  28.     }  
  29.   
  30.   
  31.     @SuppressWarnings("NullableProblems")  
  32.     @Override  
  33.     protected void onDraw(Canvas canvas) {  
  34.         canvas.save();  
  35.         canvas.drawCircle(paintX, paintY, radius, backgroundPaint);  
  36.         canvas.restore();  
  37.   
  38.         super.onDraw(canvas);  
  39.     }  
  40.   
  41.     @SuppressWarnings("NullableProblems")  
  42.     @Override  
  43.     public boolean onTouchEvent(MotionEvent event) {  
  44.         if (attributes.isMaterial() && event.getAction() == MotionEvent.ACTION_DOWN) {  
  45.             paintX = event.getX();  
  46.             paintY = event.getY();  
  47.             if (attributes.isAutoMove())  
  48.                 startMoveRoundAnimator();  
  49.             else  
  50.                 startRoundAnimator();  
  51.         }  
  52.         return super.onTouchEvent(event);  
  53.     }  
  54.   
  55.     /** 
  56.      * ============================================================================================= 
  57.      * The Animator methods 
  58.      * ============================================================================================= 
  59.      */  
  60.   
  61.     /** 
  62.      * Start Round Animator 
  63.      */  
  64.     private void startRoundAnimator() {  
  65.         float start, end, height, width;  
  66.         long time = (long) (ANIMATION_TIME * 1.85);  
  67.   
  68.         //Height Width  
  69.         height = getHeight();  
  70.         width = getWidth();  
  71.   
  72.         //Start End  
  73.         if (height < width) {  
  74.             start = height;  
  75.             end = width;  
  76.         } else {  
  77.             start = width;  
  78.             end = height;  
  79.         }  
  80.   
  81.         float startRadius = (start / 2 > paintY ? start - paintY : paintY) * 1.15f;  
  82.         float endRadius = (end / 2 > paintX ? end - paintX : paintX) * 0.85f;  
  83.   
  84.         //If The approximate square approximate square  
  85.         if (startRadius > endRadius) {  
  86.             startRadius = endRadius * 0.6f;  
  87.             endRadius = endRadius / 0.8f;  
  88.             time = (long) (time * 0.5);  
  89.         }  
  90.   
  91.         AnimatorSet set = new AnimatorSet();  
  92.         set.playTogether(  
  93.                 ObjectAnimator.ofFloat(this, mRadiusProperty, startRadius, endRadius),  
  94.                 ObjectAnimator.ofObject(this, mBackgroundColorProperty, argbEvaluator, attributes.getColor(1), attributes.getColor(2))  
  95.         );  
  96.         // set Time  
  97.         set.setDuration((long) (time / end * endRadius));  
  98.         set.setInterpolator(ANIMATION_INTERPOLATOR);  
  99.         set.start();  
  100.     }  
  101.   
  102.     /** 
  103.      * Start Move Round Animator 
  104.      */  
  105.     private void startMoveRoundAnimator() {  
  106.         float start, end, height, width, speed = 0.3f;  
  107.         long time = ANIMATION_TIME;  
  108.   
  109.         //Height Width  
  110.         height = getHeight();  
  111.         width = getWidth();  
  112.   
  113.         //Start End  
  114.         if (height < width) {  
  115.             start = height;  
  116.             end = width;  
  117.         } else {  
  118.             start = width;  
  119.             end = height;  
  120.         }  
  121.         start = start / 2 > paintY ? start - paintY : paintY;  
  122.         end = end * 0.8f / 2f;  
  123.   
  124.         //If The approximate square approximate square  
  125.         if (start > end) {  
  126.             start = end * 0.6f;  
  127.             end = end / 0.8f;  
  128.             time = (long) (time * 0.65);  
  129.             speed = 1f;  
  130.         }  
  131.   
  132.         //PaintX  
  133.         ObjectAnimator aPaintX = ObjectAnimator.ofFloat(this, mPaintXProperty, paintX, width / 2);  
  134.         aPaintX.setDuration(time);  
  135.         //PaintY  
  136.         ObjectAnimator aPaintY = ObjectAnimator.ofFloat(this, mPaintYProperty, paintY, height / 2);  
  137.         aPaintY.setDuration((long) (time * speed));  
  138.         //Radius  
  139.         ObjectAnimator aRadius = ObjectAnimator.ofFloat(this, mRadiusProperty, start, end);  
  140.         aRadius.setDuration(time);  
  141.         //Background  
  142.         ObjectAnimator aBackground = ObjectAnimator.ofObject(this, mBackgroundColorProperty, argbEvaluator, attributes.getColor(1), attributes.getColor(2));  
  143.         aBackground.setDuration(time);  
  144.   
  145.         //AnimatorSet  
  146.         AnimatorSet set = new AnimatorSet();  
  147.         set.playTogether(aPaintX, aPaintY, aRadius, aBackground);  
  148.         set.setInterpolator(ANIMATION_INTERPOLATOR);  
  149.         set.start();  
  150.     }  
  151.   
  152.   
  153.     /** 
  154.      * ============================================================================================= 
  155.      * The custom properties 
  156.      * ============================================================================================= 
  157.      */  
  158.   
  159.     private Property<MaterialButton, Float> mPaintXProperty = new Property<MaterialButton, Float>(Float.class, "paintX") {  
  160.         @Override  
  161.         public Float get(MaterialButton object) {  
  162.             return object.paintX;  
  163.         }  
  164.   
  165.         @Override  
  166.         public void set(MaterialButton object, Float value) {  
  167.             object.paintX = value;  
  168.         }  
  169.     };  
  170.   
  171.     private Property<MaterialButton, Float> mPaintYProperty = new Property<MaterialButton, Float>(Float.class, "paintY") {  
  172.         @Override  
  173.         public Float get(MaterialButton object) {  
  174.             return object.paintY;  
  175.         }  
  176.   
  177.         @Override  
  178.         public void set(MaterialButton object, Float value) {  
  179.             object.paintY = value;  
  180.         }  
  181.     };  
  182.   
  183.     private Property<MaterialButton, Float> mRadiusProperty = new Property<MaterialButton, Float>(Float.class, "radius") {  
  184.         @Override  
  185.         public Float get(MaterialButton object) {  
  186.             return object.radius;  
  187.         }  
  188.   
  189.         @Override  
  190.         public void set(MaterialButton object, Float value) {  
  191.             object.radius = value;  
  192.             invalidate();  
  193.         }  
  194.     };  
  195.   
  196.     private Property<MaterialButton, Integer> mBackgroundColorProperty = new Property<MaterialButton, Integer>(Integer.class, "bg_color") {  
  197.         @Override  
  198.         public Integer get(MaterialButton object) {  
  199.             return object.backgroundPaint.getColor();  
  200.         }  
  201.   
  202.         @Override  
  203.         public void set(MaterialButton object, Integer value) {  
  204.             object.backgroundPaint.setColor(value);  
  205.         }  
  206.     };  
  207.   
  208. }  


在最后附上两种方式运行后的效果对比图:




还不错吧?
要是感觉比较和你的胃口,这里有我的整个项目:

Genius-Android


APK与动画MP4

[Material Design] MaterialButton 效果进阶 动画自动移动进行对齐效果