效果:
主要涉及到貝塞爾曲線 自定義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!