天天看點

Android開發之使用貝塞爾曲線實作黏性水珠下拉效果Android開發之使用貝塞爾曲線實作黏性水珠下拉效果

Android開發之使用貝塞爾曲線實作黏性水珠下拉效果

标簽: 貝塞爾曲線

簡介

網上關于貝塞爾曲線的部落格和教程很多,通常講到的三點确定一條曲線:起點,終點,輔助點。

  • 常見的貝塞爾黏性效果
    Android開發之使用貝塞爾曲線實作黏性水珠下拉效果Android開發之使用貝塞爾曲線實作黏性水珠下拉效果
  • 常見的各階貝塞爾曲線
    Android開發之使用貝塞爾曲線實作黏性水珠下拉效果Android開發之使用貝塞爾曲線實作黏性水珠下拉效果
    Android開發之使用貝塞爾曲線實作黏性水珠下拉效果Android開發之使用貝塞爾曲線實作黏性水珠下拉效果
    Android開發之使用貝塞爾曲線實作黏性水珠下拉效果Android開發之使用貝塞爾曲線實作黏性水珠下拉效果

實作效果

本文所要講的黏性下拉實作效果如下:

Android開發之使用貝塞爾曲線實作黏性水珠下拉效果Android開發之使用貝塞爾曲線實作黏性水珠下拉效果
Android開發之使用貝塞爾曲線實作黏性水珠下拉效果Android開發之使用貝塞爾曲線實作黏性水珠下拉效果

效果計算分析

Android開發之使用貝塞爾曲線實作黏性水珠下拉效果Android開發之使用貝塞爾曲線實作黏性水珠下拉效果

上圖中,分别有四個點,

左邊:開始點,

上邊:控制點,

下邊:結束點,

中間:圓心。

是以可看出,該貝塞爾曲線實際上就是一個二階貝塞爾曲線(一個控制點)。各點的位置計算以及角度在稍後的代碼中将做提供。

代碼部分

PullView.java

import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.animation.PathInterpolatorCompat;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;

/**
 * Created by shenhua on 2017/10/21.
 * Email [email protected]
 */
public class PullView extends View {

    private Paint mCirclePaint;//圓的畫筆
    private int mCircleRadius = ;//圓的半徑
    private float mCirclePointX;//圓的xy坐标
    private float mCirclePointY;
    private float mProgress;//進度
    private int mDragHeight = ;//可拖拽高度
    private int mTargetWidth = ;//目标寬度
    private Path mPath = new Path();//貝塞爾曲線
    private Paint mPathPaint;
    private int mTargetGravityHeight = ;//重心點最終高度,決定控制點的Y坐标
    private int mTangentAngle = ;//角度變換 0-135
    private Interpolator mProgressInterpolator = new DecelerateInterpolator();//進度插值器
    private Interpolator mTangentAngleInterpolator;//切角路徑插值器
    private Drawable mContent = null;//圓圈内部的圈
    private int mContentMargin = ;//圈的邊距
    private ValueAnimator valueAnimator;//釋放動畫

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

    public PullView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, );
    }

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

    private void init() {
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setAntiAlias(true);//抗鋸齒
        paint.setDither(true);//防抖動
        paint.setStyle(Paint.Style.FILL);//填充方式
        paint.setColor(ContextCompat.getColor(getContext(), R.color.colorAccent));
        mCirclePaint = paint;

        paint = new Paint(Paint.ANTI_ALIAS_FLAG);//初始化路徑部分畫筆
        paint.setAntiAlias(true);
        paint.setDither(true);
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(ContextCompat.getColor(getContext(), R.color.colorAccent));
        mPathPaint = paint;

        mTangentAngleInterpolator = PathInterpolatorCompat.create((mCircleRadius * f) / mDragHeight,
                f / mTangentAngle
        );

        mContent = getResources().getDrawable(R.drawable.circle_drawable);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        updatePathLayout();
    }

    private void updatePathLayout() {
        final float progress = mProgressInterpolator.getInterpolation(mProgress);
        //擷取可繪制區域高度寬度
        final float w = getValueByLine(getWidth(), mTargetWidth, mProgress);
        final float h = getValueByLine(, mDragHeight, mProgress);
        //X對稱軸的參數,圓的圓心坐标,半徑等
        final float cPointX = w / ;
        final float cRadius = mCircleRadius;
        final float cPointY = h - cRadius;
        //控制點結束Y坐标
        final float endControlY = mTargetGravityHeight;
        mCirclePointX = cPointX;
        mCirclePointY = cPointY;

        final Path path = mPath;
        //重置
        path.reset();
        path.moveTo(, );
        //左邊部分的結束點和控制點
        float lEndPointX, lEndPointY;
        float lControlPointX, lControlPointY;
        //角度轉弧度
        float angle = mTangentAngle * mTangentAngleInterpolator.getInterpolation(progress);
        double radian = Math.toRadians(angle);
        float x = (float) (Math.sin(radian) * cRadius);
        float y = (float) (Math.cos(radian) * cRadius);

        lEndPointX = cPointX - x;
        lEndPointY = cPointY + y;

        //控制點y坐标變化
        lControlPointY = getValueByLine(, endControlY, progress);
        //控制點與結束定之前的高度
        float tHeight = lEndPointY - lControlPointY;
        //控制點與x坐标的距離
        float tWidth = (float) (tHeight / Math.tan(radian));
        lControlPointX = lEndPointX - tWidth;
        //左邊貝塞爾曲線
        path.quadTo(lControlPointX, lControlPointY, lEndPointX, lEndPointY);
        //連接配接到右邊
        path.lineTo(cPointX + (cPointX - lEndPointX), lEndPointY);
        //右邊貝塞爾曲線
        path.quadTo(cPointX + cPointX - lControlPointX, lControlPointY, w, );
        //更新内容部分Drawable
        updateContentLayout(cPointX, cPointY, cRadius);
    }

    /**
     * 對内容部分進行測量并設定
     *
     * @param cx     cPointX
     * @param cy     cPointY
     * @param radius cRadius
     */
    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;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);

        int iHeight = (int) ((mDragHeight * mProgress) + getPaddingTop() + getPaddingBottom());
        int iWidth =  * mCircleRadius + getPaddingLeft() + getPaddingRight();
        int measureWidth, measureHeight;

        if (widthMode == MeasureSpec.EXACTLY) {
            measureWidth = width;
        } else if (widthMode == MeasureSpec.AT_MOST) {
            measureWidth = Math.min(iWidth, width);
        } else {
            measureWidth = iWidth;
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            measureHeight = height;
        } else if (heightMode == MeasureSpec.AT_MOST) {
            measureHeight = Math.min(iHeight, height);
        } else {
            measureHeight = iHeight;
        }

        setMeasuredDimension(measureWidth, measureHeight);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int count = canvas.save();
        float tranX = (getWidth() - getValueByLine(getWidth(), mTargetWidth, mProgress)) / ;
        canvas.translate(tranX, );
        canvas.drawPath(mPath, mPathPaint);
        //畫圓
        canvas.drawCircle(mCirclePointX, mCirclePointY, mCircleRadius, mCirclePaint);
        Drawable drawable = mContent;
        if (drawable != null) {
            canvas.save();
            //剪切矩形區域
            canvas.clipRect(drawable.getBounds());
            //繪制
            drawable.draw(canvas);
            canvas.restore();
        }
        canvas.restoreToCount(count);
    }

    /**
     * 設定進度
     *
     * @param progress 進度
     */
    public void setProgress(float progress) {
        mProgress = progress;
        requestLayout();
    }

    /**
     * 添加釋放動作
     */
    public void release() {
        if (valueAnimator == null) {
            ValueAnimator animator = ValueAnimator.ofFloat(mProgress, f);
            animator.setInterpolator(new DecelerateInterpolator());
            animator.setDuration();
            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, f);
        }
        valueAnimator.start();
    }
}
           

circle_drawable.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <solid android:color="#7FFFFFFF" />
</shape>
           

activity_main.xml

<?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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.shenhua.bezier_demo.PullView
        android:id="@+id/pullView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</LinearLayout>
           

MainActivity.java

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

public class MainActivity extends AppCompatActivity {

    PullView pullView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        pullView = (PullView) findViewById(R.id.pullView);
    }

    private float mTouchStartY;
    private static final float TOUCH_MOVE_MAX_Y = ;
    private static final float SLIPPAGE_FACTOR = f;// 拖動阻力因子 0~1

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mTouchStartY = event.getY();
                return true;
            case MotionEvent.ACTION_MOVE:
                float y = event.getY();
                if (y >= mTouchStartY) {
                    float moveSize = (y - mTouchStartY) * SLIPPAGE_FACTOR;
                    float progress = moveSize >= TOUCH_MOVE_MAX_Y ?  : (moveSize / TOUCH_MOVE_MAX_Y);
                    pullView.setProgress(progress);
                }
                return true;
            case MotionEvent.ACTION_UP:
                pullView.release();
                return true;
            default:
                break;

        }
        return false;
    }
}
           

總結

貝塞爾曲線在Android中用起來并不難,通常的使用到二階貝塞爾曲線的創意組合就能實作很多酷炫的效果,曲線的變化就成了很重要的了,需要有很大創意,才能将貝塞爾曲線利用到最完美。

繼續閱讀