效果:
主要涉及到贝塞尔曲线 自定义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!