Android貝塞爾曲線粘性下拉刷新仿QQ粘性拉動效果

效果:

這裏寫圖片描述

主要涉及到貝塞爾曲線 自定義View 屬性動畫
代碼看註釋 寫得挺清楚了

attr_pull.xml

<resources>
    <declare-styleable name="TouchPullView">
        <attr name="pColor" format="color"/>
        <attr name="pRadius" format="dimension"/>
        <attr name="pDragHeight" format="dimension"/>
        <attr name="pTangentAngle" format="integer"/>
        <attr name="pTargetWidth" format="dimension"/>
        <attr name="pTargetGravityHeight" format="dimension"/>
        <attr name="pContentDrawable" format="reference"/>
        <attr name="pContentDrawableMargin" format="dimension"/>
    </declare-styleable>

創建shape: ic_draw_circle.xml

<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval"
    >
<solid android:color="#80ffffff"/>
</shape>

創建TouchPullView繼承View

import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.drawable.Drawable;
import android.nfc.Tag;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.v4.view.animation.PathInterpolatorCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;

/**
 * Created by wxy on 2017/12/8.
 */

public class TouchPullView extends View {
    //圓的畫筆
    private Paint mCirclePaint;
    //圓的半徑
    private float mCircleRadius = 50;
    private float mCirclePaintX, mCirclePaintY;

    private float mProgress;
    //可拖動的高度
    private int mDragHright = 300;

    //目標寬度
    private int mTargetWidth = 400;
    //貝塞爾曲線的路徑以及畫筆
    private Path mPath = new Path();
    private Paint mPathPaint;
    //重心點最終高度,決定控制點的y座標
    private int mTargetGravityHeight = 10;
    //角度變換 0 — 135度
    private int mTargentAngle = 105;

    private Interpolator mProgressInterpolator = new DecelerateInterpolator();
    private Interpolator mTanentAngleInterpolator;

    private Drawable mContent = null;
    private int mContentMargin = 0;

    public TouchPullView(Context context) {
        super(context);
        init(null);
    }


    public TouchPullView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(attrs);
    }

    public TouchPullView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(attrs);
    }


    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public TouchPullView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(attrs);
    }

    //初始化
    private void init( AttributeSet attrs) {
        //得到用戶設置的參數
        final Context context = getContext();
        TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.TouchPullView,0,0);
        int color = array.getColor(R.styleable.TouchPullView_pColor,0x20000000);

        mCircleRadius = array.getDimension(R.styleable.TouchPullView_pRadius,mCircleRadius);
        mDragHright = array.getDimensionPixelOffset(R.styleable.TouchPullView_pDragHeight,mDragHright);
        mTargentAngle = array.getInteger(R.styleable.TouchPullView_pTangentAngle,100);
        mTargetWidth = array.getDimensionPixelOffset(R.styleable.TouchPullView_pTargetWidth,mTargetWidth);
        mTargetGravityHeight = array.getDimensionPixelOffset(R.styleable.TouchPullView_pTargetGravityHeight,mTargetGravityHeight);

        mContent = array.getDrawable(R.styleable.TouchPullView_pContentDrawable);
        mContentMargin = array.getDimensionPixelOffset(R.styleable.TouchPullView_pContentDrawableMargin,0);

        Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
        //抗鋸齒
        p.setAntiAlias(true);
        //防抖動
        p.setDither(true);
        //設置爲填充模式
        p.setStyle(Paint.Style.FILL);
        p.setColor(0xFFFF4081);
        mCirclePaint = p;
        //初始化路徑部分畫筆
        p = new Paint(Paint.ANTI_ALIAS_FLAG);
        //抗鋸齒
        p.setAntiAlias(true);
        //防抖動
        p.setDither(true);
        //設置爲填充模式
        p.setStyle(Paint.Style.FILL);
        p.setColor(0xFFFF4081);
        mPathPaint = p;

        //切角路徑差值器
        mTanentAngleInterpolator = PathInterpolatorCompat.create(
                (mCircleRadius * 2.0f) / mDragHright,
                90.0f / mTargentAngle
        );


        //銷燬
        array.recycle();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //進行基礎座標參數系改變
        int count = canvas.save();
        float tranX = (getWidth() - getValueByLine(getWidth(), mTargetWidth, mProgress)) / 2;
        canvas.translate(tranX, 0);

        //畫貝塞爾曲線
        canvas.drawPath(mPath, mPathPaint);

        //畫圓
        canvas.drawCircle(mCirclePaintX, mCirclePaintY, mCircleRadius, mCirclePaint);

        Drawable drawable = mContent;
        if (drawable!=null){

            canvas.save();
            //剪切矩形區域
            canvas.clipRect(drawable.getBounds());
              //繪製回執Drawable
            drawable.draw(canvas);

            canvas.restore();
        }

        canvas.restoreToCount(count);
    }

    /**
     * 進行測量時觸發
     * 參數有兩個意圖:1.最大限度值 2.確信值 要設置的當前值
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);


        int iWidth = (int) (2 * mCircleRadius + getPaddingLeft() + getPaddingRight());
        int iHeight = (int) ((mDragHright * mProgress + 0.5f) + getPaddingTop() + getPaddingBottom());

        int mesureWidth, mesureHeight;

        if (widthMode == MeasureSpec.EXACTLY) {
            //確定的值
            mesureWidth = width;
        } else if (widthMode == MeasureSpec.AT_MOST) {

            //最多
            mesureWidth = Math.min(iWidth, width);
        } else {
            mesureWidth = iWidth;
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            //確定的值
            mesureHeight = height;
        } else if (heightMode == MeasureSpec.AT_MOST) {

            //最多
            mesureHeight = Math.min(iHeight, height);
        } else {
            mesureHeight = iHeight;
        }
        //設置測量的寬度和高度
        setMeasuredDimension(mesureWidth, mesureHeight);

    }



    /**
     * 當大小改變時觸發
     *
     * @param w
     * @param h
     * @param oldw
     * @param oldh
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //當高度變化時 路徑更新
        upadatePathLayout();
    }

    /**
     * 設置進度
     *
     * @param progress
     */
    public void setProgress(float progress) {
        Log.e("TAG:", "P:" + progress);
        //設置進度
        mProgress = progress;
        //請求重新測量
        requestLayout();
    }

    /**
     * 更新路徑等相關操作
     */
    private void upadatePathLayout() {
        //獲取進度
        final float progress = mProgressInterpolator.getInterpolation(mProgress);


        //可繪製區域高度寬度
        final float w = getValueByLine(getWidth(), mTargetWidth, mProgress);
        final float h = getValueByLine(0, mDragHright, mProgress);
        //x對稱軸的參數 圓的圓心X
        final float cPointX = w / 2;
        //圓的半徑
        final float cRadius = mCircleRadius;
        //圓的圓心Y座標
        final float cPointY = h - cRadius;
        //控制點結束Y的值
        final float endConterolY = mTargetGravityHeight;

        //更新圓的座標
        mCirclePaintX = cPointX;
        mCirclePaintY = cPointY;
        //路徑
        final Path path = mPath;
        //復位操作
        path.reset();
        path.moveTo(0, 0);


        //左邊部分的結束點和控制點
        float lEndPointX, lEndPointY;
        float lControlPointX, lControlPointY;
        float angle =mTargentAngle* mTanentAngleInterpolator.getInterpolation(progress);
        //獲取當前切線的弧度
        double radion = Math.toRadians(angle);
        float x = (float) (Math.sin(radion) * cRadius);
        float y = (float) (Math.cos(radion) * cRadius);

        //結束點
        lEndPointX = cPointX - x;
        lEndPointY = cPointY + y;
        //控制點的Y軸變化
        lControlPointY = getValueByLine(0, endConterolY, progress);
        //控制點與結束點之間的高度
        float tHeight = lEndPointY - lControlPointY;
        //控制點與X的座標距離
        float tWidth = (float) (tHeight / Math.tan(radion));
        lControlPointX = lEndPointX - tWidth;

        //左邊的貝塞爾曲線
        path.quadTo(lControlPointX, lControlPointY, lEndPointX, lEndPointY);
        //鏈接到右邊
        path.lineTo(cPointX + (cPointX - lEndPointX), lEndPointY);
        //右邊的貝塞爾曲線
        path.quadTo(cPointX + cPointX - lControlPointX, lControlPointY, w, 0);
        //更新內容部分Drawble
        updateContentLayout(cPointX,cPointY,cRadius);
    }

    /**
     * 對內容部分進行測量並設置
     * @param cx 圓心X
     * @param cy 圓心Y
     * @param radius 半徑
     */
    private void updateContentLayout(float cx,float cy,float radius){
        Drawable drawable = mContent;
        if (drawable!=null){
            int margin = mContentMargin;
            int l = (int) (cx - radius+margin);
            int r = (int) (cx+radius-margin);
            int t = (int) (cy-radius+margin);
            int b = (int) (cy+radius-margin);
            drawable.setBounds(l,t,r,b);

        }


    }


    /**
     * 獲取當前值
     *
     * @param start    起始值
     * @param end      結束值
     * @param progress 進度值
     * @return 當前進度的值
     */
    private float getValueByLine(float start, float end, float progress) {

        return start + (end - start) * progress;
    }

    private ValueAnimator valueAnimator;

    /**
     * 添加釋放操作
     */
    public void release() {
        if (valueAnimator == null) {
            ValueAnimator animator = ValueAnimator.ofFloat(mProgress, 0f);
            animator.setInterpolator(new DecelerateInterpolator());
            animator.setDuration(400);
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    Object val = animation.getAnimatedValue();
                    if (val instanceof Float) {
                        setProgress((Float) val);
                    }

                }
            });
            valueAnimator = animator;
        } else {
            valueAnimator.cancel();
            valueAnimator.setFloatValues(mProgress, 0f);
        }
        valueAnimator.start();
    }


}

MainActivity

import android.os.Bundle;
import android.support.constraint.ConstraintLayout;
import android.support.v7.app.AppCompatActivity;
import android.view.MotionEvent;
import android.view.View;

public class MainActivity extends AppCompatActivity {
    private static final float TOUCH_MOVE_MAX_Y = 600;
    private TouchPullView touchPull;
    private ConstraintLayout activity_main;
    private float mTouMoveStartY = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();

    }

    private void initView() {
        touchPull = (TouchPullView) findViewById(R.id.touchPull);
        activity_main = (ConstraintLayout) findViewById(R.id.activity_main);
        activity_main.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int action = event.getActionMasked();
                switch (action) {
                    //按下
                    case MotionEvent.ACTION_DOWN:
                        mTouMoveStartY = event.getY();
                        return true;
                    //移動
                    case MotionEvent.ACTION_MOVE:
                        float moveSize = event.getY();
                        //如果大於按下的位置 說明是往下拉
                        if (moveSize >= mTouMoveStartY) {
                            //獲取拉取的距離
                            float progress = moveSize >= TOUCH_MOVE_MAX_Y ? 1 : moveSize / TOUCH_MOVE_MAX_Y;
                            touchPull.setProgress(progress);
                        }
                        return true;


                    default:
                        touchPull.release();
                }

                return false;
            }
        });

    }
}

佈局文件:

<android.support.constraint.ConstraintLayout 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/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.wxy.jishudiancontent.MainActivity">

    <com.example.wxy.jishudiancontent.TouchPullView
        android:id="@+id/touchPull"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:pContentDrawable="@drawable/ic_draw_circle"
        app:pContentDrawableMargin="2dp"
        app:pDragHeight="120dp"
        app:pColor="@color/colorAccent"
        app:pRadius="25dp"
        app:pTangentAngle="110"
        app:pTargetGravityHeight="4dp"
        app:pTargetWidth="200dp"
        />


</android.support.constraint.ConstraintLayout>

OK!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章