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