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 shenhuanet@126.com
 */
public class PullView extends View {

    private Paint mCirclePaint;//圆的画笔
    private int mCircleRadius = 50;//圆的半径
    private float mCirclePointX;//圆的xy坐标
    private float mCirclePointY;
    private float mProgress;//进度
    private int mDragHeight = 300;//可拖拽高度
    private int mTargetWidth = 600;//目标宽度
    private Path mPath = new Path();//贝塞尔曲线
    private Paint mPathPaint;
    private int mTargetGravityHeight = 10;//重心点最终高度,决定控制点的Y坐标
    private int mTangentAngle = 100;//角度变换 0-135
    private Interpolator mProgressInterpolator = new DecelerateInterpolator();//进度插值器
    private Interpolator mTangentAngleInterpolator;//切角路径插值器
    private Drawable mContent = null;//圆圈内部的圈
    private int mContentMargin = 10;//圈的边距
    private ValueAnimator valueAnimator;//释放动画

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

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

    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 * 2.0f) / mDragHeight,
                90.0f / 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(0, mDragHeight, mProgress);
        //X对称轴的参数,圆的圆心坐标,半径等
        final float cPointX = w / 2;
        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(0, 0);
        //左边部分的结束点和控制点
        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(0, 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, 0);
        //更新内容部分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 = 2 * 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)) / 2;
        canvas.translate(tranX, 0);
        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, 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();
    }
}

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 = 300;
    private static final float SLIPPAGE_FACTOR = 0.5f;// 拖动阻力因子 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 ? 1 : (moveSize / TOUCH_MOVE_MAX_Y);
                    pullView.setProgress(progress);
                }
                return true;
            case MotionEvent.ACTION_UP:
                pullView.release();
                return true;
            default:
                break;

        }
        return false;
    }
}

总结

贝塞尔曲线在Android中用起来并不难,通常的使用到二阶贝塞尔曲线的创意组合就能实现很多酷炫的效果,曲线的变化就成了很重要的了,需要有很大创意,才能将贝塞尔曲线利用到最完美。

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页