1. 程式人生 > >android水波紋漣漪效果的實現 ---- 入門+初步提高

android水波紋漣漪效果的實現 ---- 入門+初步提高

android水波紋漣漪效果的實現<入門+初步提高>

作為一個android開發著,水波紋效果是常見的效果,可以優化ui提高使用者的互動,在android5.0之前是不會自帶水波紋的,隨著material design的提出水波紋不僅僅被用於btn的點選還有部分ui的跳轉,讓anroid介面變得比較炫酷起來;
首先今天下午沒事幹實現了一個水波紋的demo下面先展示一下:
這裡寫圖片描述

    ok下面開始進入正題,剖析實現的程式碼:

1

.MainActivity中 沒有進行任何操作,只是button所在xml的一個載體<可以跳過>,看一下 activity_main.layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
android:paddingTop="15dp" android:paddingLeft="30dp" android:paddingRight="30dp"> <com.example.houruixiang.touchripple.widget.RippleButton android:layout_width="match_parent" android:layout_height="100dp" android:gravity="center" android:background="@drawable/bg_1"
/> <LinearLayout android:layout_marginTop="10dp" android:layout_width="match_parent" android:layout_height="50dp" android:gravity="center"> <com.example.houruixiang.touchripple.widget.RippleButton android:layout_width="150dp" android:layout_height="match_parent" android:gravity="center" android:text="click1" android:background="@color/colorAccent" /> <com.example.houruixiang.touchripple.widget.RippleButton android:layout_marginLeft="10dp" android:layout_width="150dp" android:layout_height="match_parent" android:gravity="center" android:text="click2" android:background="@color/colorPrimary" /> </LinearLayout> <com.example.houruixiang.touchripple.widget.RippleButton android:layout_marginTop="15dp" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" /> </LinearLayout>

2

可以看到RippleButton就是 我們要實現的自定義button,但是需要注意水波紋漣漪效果的實現是繼承於drawable實現的,而rippleButton只是呼叫了其中的drwa()方法:
為了好理解先來看drawable的子類RippleDrawable的程式碼:
首先來看需要實現的方法:

public class RippleDrawable extends Drawable {
    @Override
    public void draw(Canvas canvas) {}

    @Override
    public void setAlpha(int alpha) {
        mAlpha = alpha;
        onColorOrAlphaChange();
    }

    @Override
    public int getAlpha() {
        return mAlpha;
    }

    @Override
    public void setColorFilter(ColorFilter colorFilter) {
        //濾鏡效果
    }

    @Override
    public int getOpacity() {
        //返回透明度
        if (mAlpha == 255){
            return PixelFormat.OPAQUE;
        }else if (mAlpha == 0){
            return PixelFormat.TRANSPARENT;
        }else{
            return PixelFormat.TRANSLUCENT;
        }

    }

介紹一下:
draw():用來完成控制元件的繪製
setColorFilter():實現濾鏡效果
getAlpha()/setAlpha():set/get方法 設定和返回透明度
getOpacity():同樣是返回透明度,就是把相關透明度轉化;eg:mAlpha = 255 —->PixelFormat.OPAQUE

if (mAlpha == 255){
            return PixelFormat.OPAQUE;
        }else if (mAlpha == 0){
            return PixelFormat.TRANSPARENT;
        }else{
            return PixelFormat.TRANSLUCENT;
        }

首先在RippleDrawable的構造方法中完成初始化<畫筆的初始化,顏色透明度的配置>,通過 使用者設定的透明度*畫筆的透明度 得到準確透明度;即:
if (mAlpha != 255){
int pAlpha = mPaint.getAlpha();
int realAlpha = (int) (pAlpha * (mAlpha/255f));
//設定透明度
mPaint.setAlpha(realAlpha);
}

 public RippleDrawable() {

        //抗鋸齒的畫筆
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        //設定抗鋸齒
        mPaint.setAntiAlias(true);
        //設定放防抖動
        mPaint.setDither(true);

        setRippleColor(0x60000000);
    }

  public void setRippleColor(int rippleColor) {
        this.rippleColor = rippleColor;
        onColorOrAlphaChange();
    }

   private void onColorOrAlphaChange() {
        //設定畫筆顏色
        mPaint.setColor(rippleColor);
        if (mAlpha != 255){
            int pAlpha = mPaint.getAlpha();
            int realAlpha = (int) (pAlpha * (mAlpha/255f));
            //設定透明度
            mPaint.setAlpha(realAlpha);
        }
        invalidateSelf();

    }

ok畫筆顏色和透明度設定完後,來看下draw方法,顧名思義draw()就是完成繪製的過程;在這個demo中有3層bg
這裡寫圖片描述

    最下面是一個美女的bg 點選時候可以看出來 上層是一個灰色的bg ,在上面是一個灰色的圓形背景,需要注意的是上面兩層bg實在自定義drawable中實現的,需要注意透明度的計算不能再互動的過程中遮擋button原有的bg
    下面來看一個方法<程式碼中進行標註 看起來會易懂一些>:
public int getCircleAlpha(int preAlpha,int bgAlpha){

        int dAlpha = preAlpha<使用者設定的透明度> - bgAlpha<預設bg上層的bg>;
        //根據以上的透明度得到最上層的透明度應該是多少時不會遮擋
        return (int) ((dAlpha*255f)/(255f - bgAlpha));
    }
根據不同透明度繪製bg<詳細看程式碼>:
 //獲取使用者設定的透明度 就是Z
        int preAlpha = mPaint.getAlpha();
        //當前背景
        int bgAlpha = (int) (preAlpha * (mBgAlpha/255f));
        //bg + prebg運算得到得背景
        int maxCircleAlpha = getCircleAlpha(preAlpha,bgAlpha);
        int circleAlpha = (int) (maxCircleAlpha * (mCircleAlpha/255f));

        mPaint.setAlpha(bgAlpha);
        canvas.drawColor(mPaint.getColor());

        mPaint.setAlpha(circleAlpha);
        canvas.drawCircle(mRipplePointX,mRipplePointY,mRippleRadius,mPaint);

        //設定最初的透明度 保證下次進入運算不會出錯
        mPaint.setAlpha(preAlpha);

3

    下面看進入動畫的漣漪效果的邏輯:
    首先互動的過程就是點選的過程 所以需要監聽點選;應為drawable的子類中沒有onTonchEvent的方法 所以需要通過RippleButton的onTouchEvent方法中傳入參給RippleDrawable的自定義ontouch中完成down ---  move --- up --- cancel:

先看RippleButton  ;其中
    //設定重新整理介面,View中已經實現 --->原始碼 button繼承子drawable.callback
    rippleDrawable.setCallback(this);
    //設定重新整理區域--->原始碼
    rippleDrawable.setBounds(0,0,getWidth(),getHeight());
         @Override
    protected boolean verifyDrawable(Drawable who) {
        return who == rippleDrawable || super.verifyDrawable(who);
    }

以上程式碼用來解決 RippleDrawable 不能重新整理的問題 詳細請看原始碼,之後博文會有提到這裡不多說,然後繼續看RippleButton的程式碼

/**
 * Created by houruixiang on 2017/7/18.
 */

public class RippleButton extends Button {


    private RippleDrawable rippleDrawable;
    private Paint paint;


    public RippleButton(Context context) {
        this(context,null);
    }

    public RippleButton(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public RippleButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        paint = new Paint();
        rippleDrawable = new RippleDrawable();
        //設定重新整理介面,View中已經實現 --->原始碼 button繼承子drawable.callback
        rippleDrawable.setCallback(this);

        //rippleDrawable
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //設定重新整理區域--->原始碼
        rippleDrawable.setBounds(0,0,getWidth(),getHeight());
    }

    @Override
    protected boolean verifyDrawable(Drawable who) {
        return who == rippleDrawable || super.verifyDrawable(who);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        rippleDrawable.draw(canvas);
        super.onDraw(canvas);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        rippleDrawable.onTouch(event);
        return true;
    }



}
    在RippleButton的:
  @Override
    public boolean onTouchEvent(MotionEvent event) {

        rippleDrawable.onTouch(event);
        return true;
    }

4

傳參開始奠定RippleDrawble的點選監聽:

public void onTouch(MotionEvent event){
        switch (event.getActionMasked()){
            case MotionEvent.ACTION_DOWN:
                //按下
                mClickPointX = event.getX();
                mClickPointY = event.getY();
                onTouchDown(mClickPointX, mClickPointY);


                break;
            case MotionEvent.ACTION_MOVE:
                //移動

                //onTouchMove(moveX,moveY);


                break;
            case MotionEvent.ACTION_UP:
                //擡起

                onTouchUp();


                break;
            case MotionEvent.ACTION_CANCEL:
                //退出
                //onTouchCancel();

                break;

        }

    }

    public void onTouchDown(float x,float y){
        //Log.i("onTouchDown====",x + "" + y );
        //unscheduleSelf(runnable);
        mUpDone = false;
        mRipplePointX = x;
        mRipplePointY = y;
        mRippleRadius = 0;
        startEnterRunnable();

    }

    public void onTouchMove(float x,float y){

    }

    public void onTouchUp(){
        mUpDone = true;
        if (mEnterDone){
            startExitRunnable();
        }

    }

    public void onTouchCancel(){
        mUpDone = true;
        if (mEnterDone){
            startExitRunnable();
        }

    }

可以看到在ontouchDown中提到startEnterRunnable(),在onTouchUp和onTouchCancel中有startExitRunnable();那麼接著看著兩個方法:

  /**
     * 開啟進入動畫
     * */
    public void startEnterRunnable(){


        mProgress = 0;
        //mEnterDone = false;
        unscheduleSelf(exitRunnable);
        unscheduleSelf(runnable);
        scheduleSelf(runnable,SystemClock.uptimeMillis());

    }

    /**
     * 開啟退出動畫
     * */
    public void startExitRunnable(){
        mExitProgress = 0;
        unscheduleSelf(runnable);
        unscheduleSelf(exitRunnable);
        scheduleSelf(exitRunnable,SystemClock.uptimeMillis());

    }

其中drawable中有handler機制來進出佇列完成繪製:而一下方法就是分別開始排程和終止排程:

        unscheduleSelf(runnable);
        unscheduleSelf(exitRunnable);

5

那麼下面看關鍵的程式碼,進入動畫的排程 和退出動畫的排程
首先來看進入動畫:

/**進入動畫*/
    //進入動畫的進度值
    private float mProgress;
    //每次遞增的時間
    private float mEnterIncrement = 16f/360;
    //進入動畫新增插值器
    private DecelerateInterpolator mEnterInterpolator = new DecelerateInterpolator(2);
    private Runnable runnable = new Runnable() {
            @Override
            public void run() {
                mEnterDone = false;
                mCircleAlpha = 255;
                mProgress = mProgress + mEnterIncrement;
                if (mProgress > 1){
                    onEnterPrograss(1);
                    enterDone();
                    return;
                }

                float interpolation = mEnterInterpolator.getInterpolation(mProgress);
                onEnterPrograss(interpolation);
                scheduleSelf(this, SystemClock.uptimeMillis() + 16);
            }


    };

    /**進入動畫重新整理的方法
     * @parms realProgress */
    public void onEnterPrograss(float realPrograss){
        mRippleRadius = getCenter(startRadius,endRadius,realPrograss);
        mRipplePointX = getCenter(mClickPointX,mCenterPointX,realPrograss);
        mRipplePointY = getCenter(mClickPointY,mCenterPointY,realPrograss);
        mBgAlpha = (int) getCenter(0,182,realPrograss);


        invalidateSelf();
    }

    private void enterDone() {
        mEnterDone = true;
        if(mUpDone)
            startExitRunnable();
    }
    然後在看退出動畫的排程:
 /**退出動畫*/
    //退出動畫的進度值
    private float mExitProgress;
    //每次遞增的時間
    private float mExitIncrement = 16f/280;
    //退出動畫新增插值器
    private AccelerateInterpolator mExitInterpolator = new AccelerateInterpolator(2);
    private Runnable exitRunnable = new Runnable() {
        @Override
        public void run() {
            if (!mEnterDone){
                return;
            }

            mExitProgress = mExitProgress + mExitIncrement;
            if (mExitProgress > 1){
                onExitPrograss(1);
                exitDone();
                return;
            }

            float interpolation = mExitInterpolator.getInterpolation(mExitProgress);
            onExitPrograss(interpolation);
            scheduleSelf(this, SystemClock.uptimeMillis() + 16);
        }
    };


    /**退出動畫重新整理的方法
     * @parms realProgress */
    public void onExitPrograss(float realPrograss){
        //設定背景
        mBgAlpha = (int) getCenter(182,0,realPrograss);
        //設定圓形區域
        mCircleAlpha = (int) getCenter(255,0,realPrograss);

        invalidateSelf();
    }

    private void exitDone() {
        mEnterDone = false;
    }
    ok~基本的講解已經完畢 下面展示完整程式碼:
    RippleButton:
package com.example.houruixiang.touchripple.widget;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.example.houruixiang.touchripple.R;

/**
 * Created by houruixiang on 2017/7/18.
 */

public class RippleButton extends Button {


    private RippleDrawable rippleDrawable;
    private Paint paint;


    public RippleButton(Context context) {
        this(context,null);
    }

    public RippleButton(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public RippleButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        paint = new Paint();
        rippleDrawable = new RippleDrawable();
        //設定重新整理介面,View中已經實現 --->原始碼 button繼承子drawable.callback
        rippleDrawable.setCallback(this);

        //rippleDrawable
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //設定重新整理區域--->原始碼
        rippleDrawable.setBounds(0,0,getWidth(),getHeight());
    }

    @Override
    protected boolean verifyDrawable(Drawable who) {
        return who == rippleDrawable || super.verifyDrawable(who);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        rippleDrawable.draw(canvas);
        super.onDraw(canvas);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        rippleDrawable.onTouch(event);
        return true;
    }



}
    RippleDrawable:
package com.example.houruixiang.touchripple.widget;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Interpolator;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.SystemClock;
import android.util.Log;
import android.view.MotionEvent;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;

import java.security.PrivilegedExceptionAction;
import java.util.Timer;
import java.util.TimerTask;


/**
 * Created by houruixiang on 2017/7/18.
 */

public class RippleDrawable extends Drawable {

    private Paint mPaint;
    private Bitmap bitmap;
    private int rippleColor;
    private float mRipplePointX = 0;
    private float mRipplePointY = 0;
    private float mRippleRadius = 0;
    private int mAlpha = 200;

    private float mCenterPointX,mCenterPointY;
    private float mClickPointX;
    private float mClickPointY;
    //最大半徑
    private float MaxRadius;
    //開始半徑
    private float startRadius;
    //結束半徑
    private float endRadius;

    //記錄是否擡起手--->boolean
    private boolean mUpDone;
    //記錄進入動畫是否完畢
    private boolean mEnterDone;


    /**進入動畫*/
    //進入動畫的進度值
    private float mProgress;
    //每次遞增的時間
    private float mEnterIncrement = 16f/360;
    //進入動畫新增插值器
    private DecelerateInterpolator mEnterInterpolator = new DecelerateInterpolator(2);
    private Runnable runnable = new Runnable() {
            @Override
            public void run() {
                mEnterDone = false;
                mCircleAlpha = 255;
                mProgress = mProgress + mEnterIncrement;
                if (mProgress > 1){
                    onEnterPrograss(1);
                    enterDone();
                    return;
                }

                float interpolation = mEnterInterpolator.getInterpolation(mProgress);
                onEnterPrograss(interpolation);
                scheduleSelf(this, SystemClock.uptimeMillis() + 16);
            }


    };

    /**進入動畫重新整理的方法
     * @parms realProgress */
    public void onEnterPrograss(float realPrograss){
        mRippleRadius = getCenter(startRadius,endRadius,realPrograss);
        mRipplePointX = getCenter(mClickPointX,mCenterPointX,realPrograss);
        mRipplePointY = getCenter(mClickPointY,mCenterPointY,realPrograss);
        mBgAlpha = (int) getCenter(0,182,realPrograss);


        invalidateSelf();
    }

    private void enterDone() {
        mEnterDone = true;
        if(mUpDone)
            startExitRunnable();
    }


    /**退出動畫*/
    //退出動畫的進度值
    private float mExitProgress;
    //每次遞增的時間
    private float mExitIncrement = 16f/280;
    //退出動畫新增插值器
    private AccelerateInterpolator mExitInterpolator = new AccelerateInterpolator(2);
    private Runnable exitRunnable = new Runnable() {
        @Override
        public void run() {
            if (!mEnterDone){
                return;
            }

            mExitProgress = mExitProgress + mExitIncrement;
            if (mExitProgress > 1){
                onExitPrograss(1);
                exitDone();
                return;
            }

            float interpolation = mExitInterpolator.getInterpolation(mExitProgress);
            onExitPrograss(interpolation);
            scheduleSelf(this, SystemClock.uptimeMillis() + 16);
        }
    };


    /**退出動畫重新整理的方法
     * @parms realProgress */
    public void onExitPrograss(float realPrograss){
        //設定背景
        mBgAlpha = (int) getCenter(182,0,realPrograss);
        //設定圓形區域
        mCircleAlpha = (int) getCenter(255,0,realPrograss);

        invalidateSelf();
    }

    private void exitDone() {
        mEnterDone = false;
    }

    //設定漸變效果 包括半徑/bg color/圓心位置等
    public float getCenter(float start,float end,float prograss){
        return start + (end - start)*prograss;
    }





    public RippleDrawable() {

        //抗鋸齒的畫筆
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        //設定抗鋸齒
        mPaint.setAntiAlias(true);
        //設定放防抖動
        mPaint.setDither(true);

        setRippleColor(0x60000000);
    }

    //private int mPaintAlpha  = 255;
    //背景的透明度
    private int mBgAlpha;
    //圓形區域的透明度
    private int mCircleAlpha;
    @Override
    public void draw(Canvas canvas) {
        //獲取使用者設定的透明度 就是Z
        int preAlpha = mPaint.getAlpha();
        //當前背景
        int bgAlpha = (int) (preAlpha * (mBgAlpha/255f));
        //bg + prebg運算得到得背景
        int maxCircleAlpha = getCircleAlpha(preAlpha,bgAlpha);
        int circleAlpha = (int) (maxCircleAlpha * (mCircleAlpha/255f));

        mPaint.setAlpha(bgAlpha);
        canvas.drawColor(mPaint.getColor());

        mPaint.setAlpha(circleAlpha);
        canvas.drawCircle(mRipplePointX,mRipplePointY,mRippleRadius,mPaint);

        //設定最初的透明度 保證下次進入運算不會出錯
        mPaint.setAlpha(preAlpha);

    }

    public int getCircleAlpha(int preAlpha,int bgAlpha){
        int dAlpha = preAlpha - bgAlpha;
        return (int) ((dAlpha*255f)/(255f - bgAlpha));
    }



    public void onTouch(MotionEvent event){
        switch (event.getActionMasked()){
            case MotionEvent.ACTION_DOWN:
                //按下
                mClickPointX = event.getX();
                mClickPointY = event.getY();
                onTouchDown(mClickPointX, mClickPointY);


                break;
            case MotionEvent.ACTION_MOVE:
                //移動

                //onTouchMove(moveX,moveY);


                break;
            case MotionEvent.ACTION_UP:
                //擡起

                onTouchUp();


                break;
            case MotionEvent.ACTION_CANCEL:
                //退出
                //onTouchCancel();

                break;

        }

    }

    public void onTouchDown(float x,float y){
        //Log.i("onTouchDown====",x + "" + y );
        //unscheduleSelf(runnable);
        mUpDone = false;
        mRipplePointX = x;
        mRipplePointY = y;
        mRippleRadius = 0;
        startEnterRunnable();

    }

    public void onTouchMove(float x,float y){

    }

    public void onTouchUp(){
        mUpDone = true;
        if (mEnterDone){
            startExitRunnable();
        }

    }

    public void onTouchCancel(){
        mUpDone = true;
        if (mEnterDone){
            startExitRunnable();
        }

    }

    /**
     * 開啟進入動畫
     * */
    public void startEnterRunnable(){


        mProgress = 0;
        //mEnterDone = false;
        unscheduleSelf(exitRunnable);
        unscheduleSelf(runnable);
        scheduleSelf(runnable,SystemClock.uptimeMillis());

    }

    /**
     * 開啟退出動畫
     * */
    public void startExitRunnable(){
        mExitProgress = 0;
        unscheduleSelf(runnable);
        unscheduleSelf(exitRunnable);
        scheduleSelf(exitRunnable,SystemClock.uptimeMillis());

    }

    public int changeColorAlpha(int color,int alpha){
        //設定透明度
        int a = (color >> 24) & 0xFF;
        a = a * alpha/255;

        int red = Color.red(color);
        int green = Color.green(color);
        int blue = Color.blue(color);
        int argb = Color.argb(a, red, green, blue);
        return argb;
    }



    //取所繪製區域的中心點
    @Override
    protected void onBoundsChange(Rect bounds) {
        super.onBoundsChange(bounds);
        mCenterPointX  = bounds.centerX();
        mCenterPointY = bounds.centerY();

        MaxRadius = Math.max(mCenterPointX,mCenterPointY);
        startRadius = MaxRadius * 0.1f;
        endRadius = MaxRadius * 0.8f;

    }

    @Override
    public void setAlpha(int alpha) {
        mAlpha = alpha;
        onColorOrAlphaChange();

    }

    @Override
    public int getAlpha() {
        return mAlpha;
    }

    @Override
    public void setColorFilter(ColorFilter colorFilter) {
        //濾鏡效果
        if (mPaint.getColorFilter() != colorFilter){
            mPaint.setColorFilter(colorFilter);
            invalidateSelf();
        }
    }

    @Override
    public int getOpacity() {
        //返回透明度
        if (mAlpha == 255){
            return PixelFormat.OPAQUE;
        }else if (mAlpha == 0){
            return PixelFormat.TRANSPARENT;
        }else{
            return PixelFormat.TRANSLUCENT;
        }

    }

    public void setRippleColor(int rippleColor) {
        this.rippleColor = rippleColor;
        onColorOrAlphaChange();
    }

    private void onColorOrAlphaChange() {
        //設定畫筆顏色
        mPaint.setColor(rippleColor);
        if (mAlpha != 255){
            int pAlpha = mPaint.getAlpha();
            int realAlpha = (int) (pAlpha * (mAlpha/255f));
            //設定透明度
            mPaint.setAlpha(realAlpha);
        }
        invalidateSelf();

    }




}