1. 程式人生 > >實現Material Design風格的點選水波盪漾效果

實現Material Design風格的點選水波盪漾效果

自Material Design的設計理念推出以來,整個安卓的UI跟UE界幾乎發生了翻天覆地的變化,由此推出的設計理念也在一步一步的被引入到開發者的具體專案中,良心來說Google提出的這個設計理念跟相關的支援包確實也給開發者帶來極大的便利,但是不足的是,向下相容做的不是太好,好多效果使用起來一般都會要求API的版本在至少21以上,所以有些效果想要引入到專案中還是需要開發者做一些定製的,今天我來跟大家分享下MD風格中的點選水波盪漾的效果實現。

對自定義View比較熟的開發者肯定不陌生,定製一個View的時候,肯定會先定製一些相關的屬性,一般會在attrs.xml檔案中通過declare-styleable來自定義相關屬性跟name

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <!--****************引入MD風格的點選水波盪漾效果***************-->
    <declare-styleable name="MaterialLayout">
        <attr name="alpha" format="integer" />
        <attr name="alpha_step" format="integer" />
        <attr name="framerate" format="integer" />
        <attr name="duration" format="integer" />
        <attr name="mycolor" format="color" />
        <attr name="scale" format="float" />
    </declare-styleable>
</resources>
然後自定義MaterialLayout檔案繼承自RelativeLayout
/**
 * 引入Material Design風格的點選水波盪漾效果佈局
 *         2017年2月6日10:30:19
 */
public class MaterialLayout extends RelativeLayout {

    private static final int DEFAULT_RADIUS = 10;
    private static final int DEFAULT_FRAME_RATE = 10;
    private static final int DEFAULT_DURATION = 200;
    private static final int DEFAULT_ALPHA = 255;
    private static final float DEFAULT_SCALE = 0.8f;
    private static final int DEFAULT_ALPHA_STEP = 5;

    /**
     * 動畫幀率
     */
    private int mFrameRate = DEFAULT_FRAME_RATE;
    /**
     * 漸變動畫持續時間
     */
    private int mDuration = DEFAULT_DURATION;
    /**
     *
     */
    private Paint mPaint = new Paint();
    /**
     * 被點選的檢視的中心點
     */
    private Point mCenterPoint = null;
    /**
     * 檢視的Rect
     */
    private RectF mTargetRectf;
    /**
     * 起始的圓形背景半徑
     */
    private int mRadius = DEFAULT_RADIUS;
    /**
     * 最大的半徑
     */
    private int mMaxRadius = DEFAULT_RADIUS;

    /**
     * 漸變的背景色
     */
    private int mCirclelColor = Color.LTGRAY;
    /**
     * 每次重繪時半徑的增幅
     */
    private int mRadiusStep = 1;
    /**
     * 儲存使用者設定的alpha值
     */
    private int mBackupAlpha;

    /**
     * 圓形半徑針對於被點選檢視的縮放比例,預設為0.8
     */
    private float mCircleScale = DEFAULT_SCALE;
    /**
     * 顏色的alpha值, (0, 255)
     */
    private int mColorAlpha = DEFAULT_ALPHA;
    /**
     * 每次動畫Alpha的漸變遞減值
     */
    private int mAlphaStep = DEFAULT_ALPHA_STEP;

    private View mTargetView;

    /**
     * @param context
     */
    public MaterialLayout(Context context) {
        this(context, null);
    }

    public MaterialLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public MaterialLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        if (isInEditMode()) {
            return;
        }

        if (attrs != null) {
            initTypedArray(context, attrs);
        }

        initPaint();

        this.setWillNotDraw(false);
        this.setDrawingCacheEnabled(true);
    }

    private void initTypedArray(Context context, AttributeSet attrs) {
        final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MaterialLayout);
        mCirclelColor = typedArray.getColor(R.styleable.MaterialLayout_mycolor, Color.LTGRAY);
        mDuration = typedArray.getInteger(R.styleable.MaterialLayout_duration, DEFAULT_DURATION);
        mFrameRate = typedArray.getInteger(R.styleable.MaterialLayout_framerate, DEFAULT_FRAME_RATE);
        mColorAlpha = typedArray.getInteger(R.styleable.MaterialLayout_alpha, DEFAULT_ALPHA);
        mCircleScale = typedArray.getFloat(R.styleable.MaterialLayout_scale, DEFAULT_SCALE);

        typedArray.recycle();

    }

    private void initPaint() {
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(mCirclelColor);
        mPaint.setAlpha(mColorAlpha);

        // 備份alpha屬性用於動畫完成時重置
        mBackupAlpha = mColorAlpha;
    }

    /**
     * 點選的某個座標點是否在View的內部
     *
     * @param touchView
     * @param x         被點選的x座標
     * @param y         被點選的y座標
     * @return 如果點選的座標在該view內則返回true, 否則返回false
     */
    private boolean isInFrame(View touchView, float x, float y) {
        initViewRect(touchView);
        return mTargetRectf.contains(x, y);
    }

    /**
     * 獲取點中的區域,螢幕絕對座標值,這個高度值也包含了狀態列和標題欄高度
     *
     * @param touchView
     */
    private void initViewRect(View touchView) {
        int[] location = new int[2];
        touchView.getLocationOnScreen(location);
        // 檢視的區域
        mTargetRectf = new RectF(location[0], location[1], location[0] + touchView.getWidth(),
                location[1] + touchView.getHeight());

    }

    /**
     * 減去狀態列和標題欄的高度
     */
    private void removeExtraHeight() {
        int[] location = new int[2];
        this.getLocationOnScreen(location);
        // 減去兩個該佈局的top,這個top值就是狀態列的高度
        mTargetRectf.top -= location[1];
        mTargetRectf.bottom -= location[1];
        // 計算中心點座標
        int centerHorizontal = (int) (mTargetRectf.left + mTargetRectf.right) / 2;
        int centerVertical = (int) ((mTargetRectf.top + mTargetRectf.bottom) / 2);
        // 獲取中心點
        mCenterPoint = new Point(centerHorizontal, centerVertical);

    }

    private View findTargetView(ViewGroup viewGroup, float x, float y) {
        int childCount = viewGroup.getChildCount();
        // 迭代查詢被點選的目標檢視
        for (int i = 0; i < childCount; i++) {
            View childView = viewGroup.getChildAt(i);
            if (childView instanceof ViewGroup) {
                return findTargetView((ViewGroup) childView, x, y);
            } else if (isInFrame(childView, x, y)) { // 否則判斷該點是否在該View的frame內
                return childView;
            }
        }

        return null;
    }

    private boolean isAnimEnd() {
        return mRadius >= mMaxRadius;
    }

    private void calculateMaxRadius(View view) {
        // 取檢視的最長邊
        int maxLength = Math.max(view.getWidth(), view.getHeight());
        // 計算Ripple圓形的半徑
        mMaxRadius = (int) ((maxLength / 2) * mCircleScale);

        int redrawCount = mDuration / mFrameRate;
        // 計算每次動畫半徑的增值
        mRadiusStep = (mMaxRadius - DEFAULT_RADIUS) / redrawCount;
        // 計算每次alpha遞減的值
        mAlphaStep = (mColorAlpha - 100) / redrawCount;
    }

    /**
     * 處理ACTION_DOWN觸控事件, 注意這裡獲取的是Raw x, y,
     * 即螢幕的絕對座標,但是這個當螢幕中有狀態列和標題欄時就需要去掉這些高度,因此得到mTargetRectf後其高度需要減去該佈局的top起點
     * ,也就是標題欄和狀態列的總高度.
     *
     * @param event
     */
    private void deliveryTouchDownEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            mTargetView = findTargetView(this, event.getRawX(), event.getRawY());
            if (mTargetView != null) {
                removeExtraHeight();
                // 計算相關資料
                calculateMaxRadius(mTargetView);
                // 重繪檢視
                invalidate();
            }
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        deliveryTouchDownEvent(event);
        return super.onInterceptTouchEvent(event);
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        // 繪製Circle
        drawRippleIfNecessary(canvas);
    }

    private void drawRippleIfNecessary(Canvas canvas) {
        if (isFoundTouchedSubView()) {
            // 計算新的半徑和alpha值
            mRadius += mRadiusStep;
            mColorAlpha -= mAlphaStep;

            // 裁剪一塊區域,這塊區域就是被點選的View的區域.通過clipRect來獲取這塊區域,使得繪製操作只能在這個區域範圍內的進行,
            // 即使繪製的內容大於這塊區域,那麼大於這塊區域的繪製內容將不可見. 這樣保證了背景層只能繪製在被點選的檢視的區域
            canvas.clipRect(mTargetRectf);
            mPaint.setAlpha(mColorAlpha);
            // 繪製背景圓形,也就是
            canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mRadius, mPaint);
        }

        if (isAnimEnd()) {
            reset();
        } else {
            invalidateDelayed();
        }
    }

    /**
     * 傳送重繪訊息
     */
    private void invalidateDelayed() {
        this.postDelayed(new Runnable() {

            @Override
            public void run() {
                invalidate();
            }
        }, mFrameRate);
    }

    /**
     * 判斷是否找到被點選的子檢視
     *
     * @return
     */
    private boolean isFoundTouchedSubView() {
        return mCenterPoint != null && mTargetView != null;
    }

    private void reset() {
        mCenterPoint = null;
        mTargetRectf = null;
        mRadius = DEFAULT_RADIUS;
        mColorAlpha = mBackupAlpha;
        mTargetView = null;
        invalidate();
    }
}

在佈局中使用此效果的時候只需要把要實現該效果的View包裹在MaterialLayout節點下即可
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!--***********Material Design風格的點選水波盪漾效果****-->
    <com.zhuandian.qxe.utils.MyView.MaterialLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.v4.widget.SwipeRefreshLayout
            android:id="@+id/swipe_refresh"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <ListView
                android:id="@+id/listView"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:padding="2dp"></ListView>
        </android.support.v4.widget.SwipeRefreshLayout>
    </com.zhuandian.qxe.utils.MyView.MaterialLayout>

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/search"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:layout_gravity="bottom|right"
        android:layout_margin="10dp"
        android:src="@drawable/qy_taobao"
        app:backgroundTint="#631263"

        />

    <include layout="@layout/load_animation" />
</FrameLayout>


當然你也可以根據attrs.xml檔案中自定義的屬性值在MaterialLayout節點下通過引入屬性來調節此效果的具體細節包括顏色,透明度的漸變效果等