自定义Layout实现Android 5.0 Material Design的点击任意View的水波效果

  • Post author:
  • Post category:其他


前言



自从Android 5.0问世以后,它的UI风格受到了大家普遍的赞美,简单、动感十足,但是由于工作比较忙,本人对于Android 5.0并没有太多的关注。前几天在知名博主任玉刚 (

博客地址

) 帅哥的群中有同学问到实现Android 5.0 Material Design中的点击任意View时产生水波的效果,刚哥表示已经实现水波效果,但是需要过段时间才能开出来。刚好本人在昨天写了声波支付的波纹效果,于是今天按照刚哥给出的实现思路弄了一下,于是也就有了今天的文章。可能效果不是很好,分享出来一是自我学习,二也是希望分享一下思路。








从目前的一些实现来看,主要有那么两个实现思路,第一种就是自定义View,比如继承Button,在Button的onDraw里面再动态绘制一层背景,然后改变背景的大小以及颜色,达到动态效果,这种实现使用比较局限,自定义一种类型的View,那么就只有这种View能够产生波纹效果;另一种是自定义布局,然后该布局中只有一个视图,也是同样的方法绘制背景,然后动画,但是也有局限性,就是一个布局中只能放一个视图,只有这个视图能够产生水波效果!







现实的情况是我们需要所有的视图在点击时都产生波纹效果,那么问题就来了,如何实现呢?






代码实现




其实大家的实现思路都是类似的,这是适用性、复杂度的问题。




我的实现思路是自定义一个布局,然后在用户触摸该布局时,通过该触摸点的坐标找到对应的子视图,找到该视图后我们在布局的dispatchDraw函数中裁剪一块区域,并且在这块区域中绘制波纹效果,使得背景图层的半径逐渐增大、透明度逐渐减小。这样点击某个视图时它的上面就产生了一个逐渐变大、颜色变浅的背景图层,不管是任何视图都会有这个动态效果!效果完成之后清除掉背景图层即可。




直接上代码吧。



  1. /*



  2. * The MIT License (MIT)



  3. *



  4. * Copyright (c) 2015 bboyfeiyu@gmail.com ( mr.simple )



  5. *



  6. * Permission is hereby granted, free of charge, to any person obtaining a copy



  7. * of this software and associated documentation files (the “Software”), to deal



  8. * in the Software without restriction, including without limitation the rights



  9. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell



  10. * copies of the Software, and to permit persons to whom the Software is



  11. * furnished to do so, subject to the following conditions:



  12. *



  13. * The above copyright notice and this permission notice shall be included in



  14. * all copies or substantial portions of the Software.



  15. *



  16. * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR



  17. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,



  18. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE



  19. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER



  20. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,



  21. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN



  22. * THE SOFTWARE.



  23. */






  24. package


    org.simple;




  25. import


    android.content.Context;



  26. import


    android.content.res.TypedArray;



  27. import


    android.graphics.Canvas;



  28. import


    android.graphics.Color;



  29. import


    android.graphics.Paint;



  30. import


    android.graphics.Point;



  31. import


    android.graphics.RectF;



  32. import


    android.util.AttributeSet;



  33. import


    android.view.MotionEvent;



  34. import


    android.view.View;



  35. import


    android.view.ViewGroup;



  36. import


    android.widget.RelativeLayout;




  37. import


    org.simple.materiallayout.R;




  38. /**



  39. * MaterialLayout是模拟Android 5.0中View被点击的波纹效果的布局,与其他的模拟Material



  40. * Desigin效果的View不同,所有在MaterialLayout布局下的子视图被点击时都会产生波纹效果,而不是某个特定的View才会有这样的效果.



  41. *



  42. * @author mrsimple



  43. */





  44. public




    class


    MaterialLayout


    extends


    RelativeLayout {




  45. private




    static




    final




    int


    DEFAULT_RADIUS =


    10


    ;



  46. private




    static




    final




    int


    DEFAULT_FRAME_RATE =


    10


    ;



  47. private




    static




    final




    int


    DEFAULT_DURATION =


    200


    ;



  48. private




    static




    final




    int


    DEFAULT_ALPHA =


    255


    ;



  49. private




    static




    final




    float


    DEFAULT_SCALE =


    0


    .8f;



  50. private




    static




    final




    int


    DEFAULT_ALPHA_STEP =


    5


    ;




  51. /**



  52. * 动画帧率



  53. */





  54. private




    int


    mFrameRate = DEFAULT_FRAME_RATE;



  55. /**



  56. * 渐变动画持续时间



  57. */





  58. private




    int


    mDuration = DEFAULT_DURATION;



  59. /**



  60. *



  61. */





  62. private


    Paint mPaint =


    new


    Paint();



  63. /**



  64. * 被点击的视图的中心点



  65. */





  66. private


    Point mCenterPoint =


    null


    ;



  67. /**



  68. * 视图的Rect



  69. */





  70. private


    RectF mTargetRectf;



  71. /**



  72. * 起始的圆形背景半径



  73. */





  74. private




    int


    mRadius = DEFAULT_RADIUS;



  75. /**



  76. * 最大的半径



  77. */





  78. private




    int


    mMaxRadius = DEFAULT_RADIUS;




  79. /**



  80. * 渐变的背景色



  81. */





  82. private




    int


    mCirclelColor = Color.LTGRAY;



  83. /**



  84. * 每次重绘时半径的增幅



  85. */





  86. private




    int


    mRadiusStep =


    1


    ;



  87. /**



  88. * 保存用户设置的alpha值



  89. */





  90. private




    int


    mBackupAlpha;




  91. /**



  92. * 圆形半径针对于被点击视图的缩放比例,默认为0.8



  93. */





  94. private




    float


    mCircleScale = DEFAULT_SCALE;



  95. /**



  96. * 颜色的alpha值, (0, 255)



  97. */





  98. private




    int


    mColorAlpha = DEFAULT_ALPHA;



  99. /**



  100. * 每次动画Alpha的渐变递减值



  101. */





  102. private




    int


    mAlphaStep = DEFAULT_ALPHA_STEP;




  103. private


    View mTargetView;




  104. /**



  105. * @param context



  106. */





  107. public


    MaterialLayout(Context context) {



  108. this


    (context,


    null


    );


  109. }



  110. public


    MaterialLayout(Context context, AttributeSet attrs) {



  111. super


    (context, attrs);


  112. init(context, attrs);

  113. }



  114. public


    MaterialLayout(Context context, AttributeSet attrs,


    int


    defStyle) {



  115. super


    (context, attrs, defStyle);


  116. init(context, attrs);

  117. }



  118. private




    void


    init(Context context, AttributeSet attrs) {



  119. if


    (isInEditMode()) {



  120. return


    ;


  121. }



  122. if


    (attrs !=


    null


    ) {


  123. initTypedArray(context, attrs);

  124. }


  125. initPaint();



  126. this


    .setWillNotDraw(


    false


    );



  127. this


    .setDrawingCacheEnabled(


    true


    );


  128. }



  129. private




    void


    initTypedArray(Context context, AttributeSet attrs) {



  130. final


    TypedArray typedArray = context.obtainStyledAttributes(attrs,


  131. R.styleable.MaterialLayout);

  132. mCirclelColor = typedArray.getColor(R.styleable.MaterialLayout_color, Color.LTGRAY);

  133. mDuration = typedArray.getInteger(R.styleable.MaterialLayout_duration,

  134. DEFAULT_DURATION);

  135. mFrameRate = typedArray

  136. .getInteger(R.styleable.MaterialLayout_framerate, DEFAULT_FRAME_RATE);

  137. mColorAlpha = typedArray.getInteger(R.styleable.MaterialLayout_alpha, DEFAULT_ALPHA);

  138. mCircleScale = typedArray.getFloat(R.styleable.MaterialLayout_scale, DEFAULT_SCALE);


  139. typedArray.recycle();


  140. }



  141. private




    void


    initPaint() {


  142. mPaint.setAntiAlias(

    true


    );


  143. mPaint.setStyle(Paint.Style.FILL);

  144. mPaint.setColor(mCirclelColor);

  145. mPaint.setAlpha(mColorAlpha);



  146. // 备份alpha属性用于动画完成时重置




  147. mBackupAlpha = mColorAlpha;

  148. }



  149. /**



  150. * 点击的某个坐标点是否在View的内部



  151. *



  152. * @param touchView



  153. * @param x 被点击的x坐标



  154. * @param y 被点击的y坐标



  155. * @return 如果点击的坐标在该view内则返回true,否则返回false



  156. */





  157. private




    boolean


    isInFrame(View touchView,


    float


    x,


    float


    y) {


  158. initViewRect(touchView);


  159. return


    mTargetRectf.contains(x, y);


  160. }



  161. /**



  162. * 获取点中的区域,屏幕绝对坐标值,这个高度值也包含了状态栏和标题栏高度



  163. *



  164. * @param touchView



  165. */





  166. private




    void


    initViewRect(View touchView) {



  167. int


    [] location =


    new




    int


    [


    2


    ];


  168. touchView.getLocationOnScreen(location);


  169. // 视图的区域




  170. mTargetRectf =

    new


    RectF(location[


    0


    ], location[


    1


    ], location[


    0


    ]


  171. + touchView.getWidth(), location[

    1


    ] + touchView.getHeight());



  172. }



  173. /**



  174. * 减去状态栏和标题栏的高度



  175. */





  176. private




    void


    removeExtraHeight() {



  177. int


    [] location =


    new




    int


    [


    2


    ];



  178. this


    .getLocationOnScreen(location);



  179. // 减去两个该布局的top,这个top值就是状态栏的高度




  180. mTargetRectf.top -= location[

    1


    ];


  181. mTargetRectf.bottom -= location[

    1


    ];



  182. // 计算中心点坐标





  183. int


    centerHorizontal = (


    int


    ) (mTargetRectf.left + mTargetRectf.right) /


    2


    ;



  184. int


    centerVertical = (


    int


    ) ((mTargetRectf.top + mTargetRectf.bottom) /


    2


    );



  185. // 获取中心点




  186. mCenterPoint =

    new


    Point(centerHorizontal, centerVertical);



  187. }



  188. private


    View findTargetView(ViewGroup viewGroup,


    float


    x,


    float


    y) {



  189. int


    childCount = viewGroup.getChildCount();



  190. // 迭代查找被点击的目标视图





  191. for


    (


    int


    i =


    0


    ; i < childCount; i++) {


  192. View childView = viewGroup.getChildAt(i);


  193. if


    (childView


    instanceof


    ViewGroup) {



  194. return


    findTargetView((ViewGroup) childView, x, y);


  195. }

    else




    if


    (isInFrame(childView, x, y)) {


    // 否则判断该点是否在该View的frame内





  196. return


    childView;


  197. }

  198. }



  199. return




    null


    ;


  200. }



  201. private




    boolean


    isAnimEnd() {



  202. return


    mRadius >= mMaxRadius;


  203. }



  204. private




    void


    calculateMaxRadius(View view) {



  205. // 取视图的最长边





  206. int


    maxLength = Math.max(view.getWidth(), view.getHeight());



  207. // 计算Ripple圆形的半径




  208. mMaxRadius = (

    int


    ) ((maxLength /


    2


    ) * mCircleScale);




  209. int


    redrawCount = mDuration / mFrameRate;



  210. // 计算每次动画半径的增值




  211. mRadiusStep = (mMaxRadius – DEFAULT_RADIUS) / redrawCount;


  212. // 计算每次alpha递减的值




  213. mAlphaStep = (mColorAlpha –

    100


    ) / redrawCount;


  214. }



  215. /**



  216. * 处理ACTION_DOWN触摸事件, 注意这里获取的是Raw x, y,



  217. * 即屏幕的绝对坐标,但是这个当屏幕中有状态栏和标题栏时就需要去掉这些高度,因此得到mTargetRectf后其高度需要减去该布局的top起点



  218. * ,也就是标题栏和状态栏的总高度.



  219. *



  220. * @param event



  221. */





  222. private




    void


    deliveryTouchDownEvent(MotionEvent event) {



  223. if


    (event.getAction() == MotionEvent.ACTION_DOWN) {


  224. mTargetView = findTargetView(

    this


    , event.getRawX(), event.getRawY());



  225. if


    (mTargetView !=


    null


    ) {


  226. removeExtraHeight();


  227. // 计算相关数据




  228. calculateMaxRadius(mTargetView);


  229. // 重绘视图




  230. invalidate();

  231. }

  232. }

  233. }



  234. @Override





  235. public




    boolean


    onInterceptTouchEvent(MotionEvent event) {


  236. deliveryTouchDownEvent(event);


  237. return




    super


    .onInterceptTouchEvent(event);


  238. }



  239. @Override





  240. protected




    void


    dispatchDraw(Canvas canvas) {



  241. super


    .dispatchDraw(canvas);



  242. // 绘制Circle




  243. drawRippleIfNecessary(canvas);

  244. }



  245. private




    void


    drawRippleIfNecessary(Canvas canvas) {



  246. if


    (isFoundTouchedSubView()) {



  247. // 计算新的半径和alpha值




  248. mRadius += mRadiusStep;

  249. mColorAlpha -= mAlphaStep;



  250. // 裁剪一块区域,这块区域就是被点击的View的区域.通过clipRect来获取这块区域,使得绘制操作只能在这个区域范围内的进行,





  251. // 即使绘制的内容大于这块区域,那么大于这块区域的绘制内容将不可见. 这样保证了背景层只能绘制在被点击的视图的区域




  252. canvas.clipRect(mTargetRectf);

  253. mPaint.setAlpha(mColorAlpha);


  254. // 绘制背景圆形,也就是




  255. canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mRadius, mPaint);

  256. }



  257. if


    (isAnimEnd()) {


  258. reset();

  259. }

    else


    {


  260. invalidateDelayed();

  261. }

  262. }



  263. /**



  264. * 发送重绘消息



  265. */





  266. private




    void


    invalidateDelayed() {



  267. this


    .postDelayed(


    new


    Runnable() {




  268. @Override





  269. public




    void


    run() {


  270. invalidate();

  271. }

  272. }, mFrameRate);

  273. }



  274. /**



  275. * 判断是否找到被点击的子视图



  276. *



  277. * @return



  278. */





  279. private




    boolean


    isFoundTouchedSubView() {



  280. return


    mCenterPoint !=


    null


    && mTargetView !=


    null


    ;


  281. }



  282. private




    void


    reset() {


  283. mCenterPoint =

    null


    ;


  284. mTargetRectf =

    null


    ;


  285. mRadius = DEFAULT_RADIUS;

  286. mColorAlpha = mBackupAlpha;

  287. mTargetView =

    null


    ;


  288. invalidate();

  289. }


  290. }







自定义的属性, attrs.xml




  1. <?


    xml




    version


    =


    “1.0”




    encoding


    =


    “utf-8”


    ?>





  2. <


    resources


    >






  3. <


    declare-styleable




    name


    =


    “MaterialLayout”


    >





  4. <


    attr




    name


    =


    “alpha”




    format


    =


    “integer”




    />





  5. <


    attr




    name


    =


    “alpha_step”




    format


    =


    “integer”




    />





  6. <


    attr




    name


    =


    “framerate”




    format


    =


    “integer”




    />





  7. <


    attr




    name


    =


    “duration”




    format


    =


    “integer”




    />





  8. <


    attr




    name


    =


    “color”




    format


    =


    “color”




    />





  9. <


    attr




    name


    =


    “scale”




    format


    =


    “float”




    />





  10. </


    declare-styleable


    >






  11. </


    resources


    >








使用示例




引用MaterialLayout工程或者将代码和attrs.xml拷贝到你的工程中,然后在你的布局xml中添加MaterialLayout布局,注意,不要忘了引用MaterialLayout自定义属性的命名空间,即下面的xmlns:ml这句。把com.example.materialdemo替换成你的包名就OK了。



  1. <


    org.simple.MaterialLayout




    xmlns:android


    =


    “http://schemas.android.com/apk/res/android”





  2. xmlns:ml


    =


    “http://schemas.android.com/apk/res/com.example.materialdemo”





  3. android:id


    =


    “@+id/layout”





  4. android:layout_width


    =


    “match_parent”





  5. android:layout_height


    =


    “match_parent”





  6. android:layout_margin


    =


    “5dp”





  7. android:background


    =


    “#f0f0f0”





  8. android:gravity


    =


    “center”





  9. ml:duration


    =


    “200”





  10. ml:alpha


    =


    “200”





  11. ml:scale


    =


    “1.2”





  12. ml:color


    =


    “#FFD306”




    >






  13. <


    Button





  14. android:id


    =


    “@+id/my_button”





  15. android:layout_width


    =


    “wrap_content”





  16. android:layout_height


    =


    “wrap_content”





  17. android:background


    =


    “#33CC99”





  18. android:padding


    =


    “10dp”





  19. android:text


    =


    “@string/click”





  20. android:textSize


    =


    “20sp”




    />






  21. <


    ImageView





  22. android:id


    =


    “@+id/my_imageview1”





  23. android:layout_width


    =


    “100dp”





  24. android:layout_height


    =


    “100dp”





  25. android:layout_below


    =


    “@id/my_button”





  26. android:layout_marginTop


    =


    “30dp”





  27. android:background


    =


    “#33CC99”





  28. android:contentDescription


    =


    “@string/app_name”





  29. android:padding


    =


    “10dp”





  30. android:src


    =


    “@drawable/ic_launcher”




    />






  31. </


    org.simple.MaterialLayout


    >





效果图






这个gif录得有点卡,真机上看起来还是不错的。大家可以到github上clone一份运行看看效果,如果觉得不行也别喷,给出你的github地址,本人也愿意学习您的优秀实现。在这里也期待刚哥早日开源出更好的