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