效果
注意 本节内容基于上一篇博客的代码继续编写code
https://blog.csdn.net/u011109881/article/details/112427623
本节知识点
1.如何创建View的截图
2.值动画练习
3.贝塞尔曲线的使用
4.画笔使用 onDraw练习
5.View.getRowX VS View.getX
6.获取状态栏高度
7.插值器使用
8.帧动画练习
步骤分析
1.原理
将原来的View隐藏,拖动的时候利用WindowManager创建一个新View(只是一个截图),拖动的其实只是截图,我们可以将截图放在Decor View的层级(这里是我的猜测,视频里面说的是放在WindowManager上 但我不理解WindowManager是哪一层),这样就能在状态栏上面拖动
2.按下的时候将原先的View隐藏
2.1创建一个截图
2.2将创建的截图保存到DragView并让其在onDraw绘制 注意保存截图的显示位置
2.3将创建的截图和DragView一起显示到界面(在onDraw绘制)
3.移动的时候 不停绘制贝塞尔曲线以及截图
3.1 遇到问题originView的位置不对,手指点的位置不对
mOriginView.getX(), mOriginView.getY()的方式不对 这个是相对于父布局的位置 我们需要相对屏幕的位置,另外需要确保固定圆在mOriginView的中心就要加上view宽高的一半,同时还要减去图状态栏高度
4.对手指抬起做监听
4.1如果 抬起时距离不大,view回弹
4.2计算回弹轨迹,添加回弹动画
4.3利用插值器,回弹到原始位置的时候添加抖动效果
4.4去除创建的截图并显示出原先的View
4.5如果View拖动很远 则触发消失的动作。将原先的View设置为Gone,隐藏拖动的截图,播放爆炸的帧动画,播放完毕释放资源
4.6通知调用者,View被删除成功
大部分代码
工具类
class Utils {
//2.1 创建截图
public static Bitmap getBitmapByView(View view) {
view.buildDrawingCache();
Bitmap bitmap = view.getDrawingCache();
return bitmap;
}
public static float dp2px(float dp, Context context) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
}
//获取状态栏高度
public static float getStatusBarHeight(Context context) {
//获取status_bar_height资源的ID
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
return context.getResources().getDimensionPixelSize(resourceId);
}
//没有获取到 取0
return 0;
}
//按照百分比在由point1 point2组成的线段上取点
public static PointF getPointByPercent(PointF point1, PointF point2, float percent) {
return new PointF(evaluateValue(percent, point1.x, point2.x), evaluateValue(
percent, point1.y, point2.y));
}
//Number是int float等基本数字类型的父类
public static float evaluateValue(float percent, Number start, Number end) {
return start.floatValue() + (end.floatValue() - start.floatValue())
* percent;
}
}
Activity
public class MainActivity extends AppCompatActivity {
private final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView = findViewById(R.id.textView);
DragBoomView.attachToView(textView, view -> {
//4.6通知到调用者,View被删除成功
Toast.makeText(MainActivity.this, "原先的View1被删除", Toast.LENGTH_SHORT).show();
view.setVisibility(View.GONE);
});
TextView textView2 = findViewById(R.id.textView2);
DragBoomView.attachToView(textView2, view -> {
Toast.makeText(MainActivity.this, "原先的View2被删除", Toast.LENGTH_SHORT).show();
view.setVisibility(View.GONE);
});
TextView textView3 = findViewById(R.id.textView3);
DragBoomView.attachToView(textView3, view -> {
Toast.makeText(MainActivity.this, "原先的View3被删除", Toast.LENGTH_SHORT).show();
view.setVisibility(View.GONE);
});
}
}
自定义View(包含截图+贝塞尔曲线)
class DragBoomView extends View {
private static final String TAG = "DragBoomView";
private PointF mFixPoint;
private PointF mFingerPoint;
private Paint mPaint;
private float mFixPointInitRadius = 10;//固定圆初始半径
private float mMinFixPointRadius = 5;//固定圆最小半径 如果比这个更小 不进行绘制
private float mFixPointChangedRadius;//挪动手指后的固定圆半径
private float mFingerPointRadius = 10;
private DragBoomViewTouchListener mDragBoomViewTouchListener;
private Bitmap mCaptureView;//截图
public DragBoomView(Context context) {
this(context, null);
}
public DragBoomView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public DragBoomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaint();
mFixPointInitRadius = Utils.dp2px(mFixPointInitRadius, context);
mFingerPointRadius = Utils.dp2px(mFingerPointRadius, context);
mMinFixPointRadius = Utils.dp2px(mMinFixPointRadius, context);
}
public static void attachToView(View textView, DragBoomViewTouchListener.DragViewDisappearListener disappearListener) {
textView.setOnTouchListener(new DragBoomViewTouchListener(textView, disappearListener));
}
private void initPaint() {
mPaint = new Paint();
mPaint.setDither(true);
mPaint.setAntiAlias(true);
mPaint.setColor(Color.RED);
}
@Override
protected void onDraw(Canvas canvas) {
if (mFingerPoint == null || mFixPoint == null) {
return;
}
//绘制固定圆
if (isNeedShowBezier()) {//当手指离固定圆太远 不绘制固定圆
canvas.drawCircle(mFixPoint.x, mFixPoint.y, mFixPointChangedRadius, mPaint);
canvas.drawPath(getBezierPath(), mPaint);
}
//绘制跟随手指的圆
canvas.drawCircle(mFingerPoint.x, mFingerPoint.y, mFingerPointRadius, mPaint);
//2.3将创建的截图和DragView一起显示到界面(在onDraw绘制)
if (mCaptureView != null) {
canvas.drawBitmap(mCaptureView, mFingerPoint.x - mCaptureView.getWidth() / 2, mFingerPoint.y - mCaptureView.getHeight() / 2, mPaint);
}
}
//计算两点之间的距离
double distanceOfPoints(PointF point1, PointF point2) {
//勾股定理
return Math.sqrt(Math.pow(point1.x - point2.x, 2) + Math.pow(point1.y - point2.y, 2));
}
// @Override
// public boolean onTouchEvent(MotionEvent event) {
// switch (event.getAction()) {
// case MotionEvent.ACTION_DOWN:
// mFixPoint = new PointF();
// mFingerPoint = new PointF();
// mFixPoint.x = event.getX();
// mFixPoint.y = event.getY();
// mFingerPoint.x = event.getX();
// mFingerPoint.y = event.getY();
// case MotionEvent.ACTION_MOVE:
// mFingerPoint.x = event.getX();
// mFingerPoint.y = event.getY();
// break;
// case MotionEvent.ACTION_UP:
// break;
// }
// //根据手指的移动 计算两点之间的距离 根据距离改变mFixPoint的半径
// mFixPointChangedRadius = calculateFixPointRadius(distanceOfPoints(mFingerPoint, mFixPoint));
// invalidate();
// return true;
// }
private float calculateFixPointRadius(double distanceOfPoints) {
return mFixPointInitRadius - (float) distanceOfPoints / 20f;//除数(20f)越大,代表半径随距离变化的程度越小
}
private Path getBezierPath() {
Path bezierPath = new Path();
//计算四点坐标
//先计算∠a ∠a = arctan((r1.y-r2.y)/(r1.x-r2.x))
double angleA = Math.atan((mFixPoint.y - mFingerPoint.y) / (mFixPoint.x - mFingerPoint.x));
//计算x y以及 x' y'
float x = (float) (Math.sin(angleA) * mFixPointChangedRadius);
float y = (float) (Math.cos(angleA) * mFixPointChangedRadius);
float x2 = (float) (Math.sin(angleA) * mFingerPointRadius);
float y2 = (float) (Math.cos(angleA) * mFingerPointRadius);
//四个点坐标分别为 A1 B1 A2 B2 他们坐标表示为
float A1x = mFixPoint.x + x;
float A1y = mFixPoint.y - y;
float B1x = mFixPoint.x - x;
float B1y = mFixPoint.y + y;
float A2x = mFingerPoint.x + x2;
float A2y = mFingerPoint.y - y2;
float B2x = mFingerPoint.x - x2;
float B2y = mFingerPoint.y + y2;
float controlPint1x = (mFixPoint.x + mFingerPoint.x) * 0.5f;
float controlPint1y = (mFixPoint.y + mFingerPoint.y) * 0.5f;
//绘制路径为A1 A2 B2 B1
bezierPath.moveTo(A1x, A1y);
bezierPath.quadTo(controlPint1x, controlPint1y, A2x, A2y);
bezierPath.lineTo(B2x, B2y);
bezierPath.quadTo(controlPint1x, controlPint1y, B1x, B1y);
bezierPath.close();
return bezierPath;
}
public void setDragViewTouchListener(DragBoomViewTouchListener dragBoomViewTouchListener) {
mDragBoomViewTouchListener = dragBoomViewTouchListener;
}
public void setCaptureView(Bitmap bitmap) {
this.mCaptureView = bitmap;
}
public void updatePosition(float rawX, float rawY) {
if (mFingerPoint == null) {
mFingerPoint = new PointF();
}
mFingerPoint.x = rawX;
mFingerPoint.y = rawY;
mFixPointChangedRadius = calculateFixPointRadius(distanceOfPoints(mFingerPoint, mFixPoint));
invalidate();
}
//2.2将创建的截图保存到DragView并让其在onDraw绘制 注意保存截图的显示位置
public void initPoints(float pointX, float pointY) {
mFixPoint = new PointF(pointX, pointY);
mFingerPoint = new PointF(pointX, pointY);
invalidate();
}
private boolean isNeedShowBezier() {
return mFixPointChangedRadius > mMinFixPointRadius;
}
public void dealWithActionUp() {
if (this.isNeedShowBezier()) { //4.1如果 抬起时距离不大,view回弹
playBackAnimate();
} else {//4.5如果View拖动很远 则触发消失的动作。将原先的View设置为Gone,隐藏拖动的截图,播放爆炸的帧动画,播放完毕释放资源
if (mDragBoomViewTouchListener != null) {
//4.5如果View拖动很远 则触发消失的动作。将原先的View设置为Gone,隐藏拖动的截图,播放爆炸的帧动画,播放完毕释放资源
mDragBoomViewTouchListener.dismiss(mFingerPoint);
}
}
}
private void playBackAnimate() {
//4.2计算回弹轨迹,添加回弹动画
//ValueAnimator 值变化的动画 getAnimatedValue由0变化到1
ValueAnimator animator = ObjectAnimator.ofFloat(1);
animator.setDuration(250);
final PointF start = new PointF(mFingerPoint.x, mFingerPoint.y);
final PointF end = new PointF(mFixPoint.x, mFixPoint.y);
animator.addUpdateListener(animation -> {
float percent = (float) animation.getAnimatedValue();// 0 - 1
PointF pointF = Utils.getPointByPercent(start, end, percent);
// 用代码更新拖拽点
updatePosition(pointF.x, pointF.y);
});
// 4.3利用插值器,回弹到原始位置的时候添加抖动效果
// 设置一个差值器 在结束的时候有一个弹动效果
animator.setInterpolator(new OvershootInterpolator(3f));//3f表示晃动强度较大 数值越大效果越强
animator.start();
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (mDragBoomViewTouchListener != null) {
//4.4去除创建的截图并显示出原先的View
// 还要通知 TouchListener 移除当前View 然后显示静态的 View
mDragBoomViewTouchListener.reset();
}
}
});
}
}
Listener
class DragBoomViewTouchListener implements View.OnTouchListener {
private DragBoomView mDragView;
private WindowManager mWindowManager;
private Context mContext;
private View mOriginView;
private WindowManager.LayoutParams mParams;
// 爆炸帧动画
private FrameLayout mBombFrame;
private ImageView mBombImage;
private DragViewDisappearListener mDisappearListener;
public DragBoomViewTouchListener(View mOriginView, DragViewDisappearListener disappearListener) {
this.mOriginView = mOriginView;
this.mContext = mOriginView.getContext();
mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
mDragView = new DragBoomView(mContext);
mDragView.setDragViewTouchListener(this);
mParams = new WindowManager.LayoutParams();
// 背景要透明
mParams.format = PixelFormat.TRANSPARENT;
//爆炸动画初始化
mBombFrame = new FrameLayout(mContext);
mBombImage = new ImageView(mContext);
mBombImage.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
mBombFrame.addView(mBombImage);
this.mDisappearListener = disappearListener;
}
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
int[] location = new int[2];
mOriginView.getLocationOnScreen(location);
//3.1 遇到问题originView的位置不对,手指点的位置不对
//location是view左上角相对屏幕的位置,需要确保固定圆在mOriginView的中心就要加上view宽高的一半,同时还要减去图状态栏高度
mDragView.initPoints(location[0] + (mOriginView.getMeasuredWidth() / 2), location[1] + (mOriginView.getMeasuredHeight() / 2) - Utils.getStatusBarHeight(mContext));
Bitmap bitmap = Utils.getBitmapByView(mOriginView);
mDragView.setCaptureView(bitmap);
mWindowManager.addView(mDragView, mParams);
//2.按下的时候将原先的View隐藏
mOriginView.setVisibility(View.INVISIBLE);
case MotionEvent.ACTION_MOVE://3.移动的时候 不停绘制贝塞尔曲线以及截图
//这里传的点和down的点不统一 我认为不太好
mDragView.updatePosition(event.getRawX(), event.getRawY() - Utils.getStatusBarHeight(mContext));
break;
case MotionEvent.ACTION_UP:
//4.对手指抬起做监听
mDragView.dealWithActionUp();
break;
}
return true;
}
public void reset() {
// 把创建的贝塞尔曲线以及截图删除
mWindowManager.removeView(mDragView);
// 把原来的View显示
mOriginView.setVisibility(View.VISIBLE);
}
//帧动画
public void dismiss(PointF pointF) {
// 移除截图的View
mWindowManager.removeView(mDragView);
// 要在 mWindowManager 添加一个爆炸动画
mWindowManager.addView(mBombFrame, mParams);
mBombImage.setBackgroundResource(R.drawable.anim_bubble_pop);
AnimationDrawable drawable = (AnimationDrawable) mBombImage.getBackground();
mBombImage.setX(pointF.x - drawable.getIntrinsicWidth() / 2);
mBombImage.setY(pointF.y - drawable.getIntrinsicHeight() / 2);
drawable.start();
// 等它执行完之后我要移除掉这个 爆炸动画也就是 mBombFrame
mBombImage.postDelayed(new Runnable() {
@Override
public void run() {
mWindowManager.removeView(mBombFrame);
// 通知一下外面该消失
if (mDisappearListener != null) {
mDisappearListener.viewDismiss(mOriginView);
}
}
}, getAnimationDrawableTime(drawable));
}
private long getAnimationDrawableTime(AnimationDrawable drawable) {
int numberOfFrames = drawable.getNumberOfFrames();
long time = 0;
for (int i = 0; i < numberOfFrames; i++) {
time += drawable.getDuration(i);
}
return time;
}
public interface DragViewDisappearListener {
void viewDismiss(View view);
}
}
帧动画
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="true">
<item
android:drawable="@drawable/pop1"
android:duration="100" />
<item
android:drawable="@drawable/pop2"
android:duration="100" />
<item
android:drawable="@drawable/pop3"
android:duration="100" />
<item
android:drawable="@drawable/pop4"
android:duration="100" />
<item
android:drawable="@drawable/pop5"
android:duration="100" />
</animation-list>
后记
1.原视频中将原先的view传递到自定义View并给他注册TouchListener,事实上这样有点问题,因为这样该View不再能注册其他TouchListener,如果在其他地方注册,就会导致拖动消失的功能消失。不过因为既然想要拖动该View,应该也不需要在其他地方注册TouchListener了吧?
2.在拖动的时候发现状态栏变成黑色,如下图。事实上,我不清楚创建的截图放到了什么位置,因为我使用Layout Inspector的时候没有看到创建的截图View在哪里
但是我想我们应该可以取得DecorView 因为DecorView本身是一个FrameLayout,我们可以将截图放在DecorView的第一个的位置(index从0开始计算),我们可以取得Decor View的引用 保存原来的View的引用,先删除掉原先的所有子view,然后重新按照顺序添加。当然,这只是一个思路,没有验证。
3.DragBoomViewTouchListener 和DragBoomView 职责不清,DragBoomViewTouchListener 明明是个listener 但是里面包含太多的功能,MainActivity普通的View通过DragBoomView与DragBoomViewTouchListener 建立联系,感觉耦合性较高,可以把更多的功能放到DragBoomView 中。
4.回弹动画应该可以使用translationX和translationY动画结合代替
private void playBackAnimate() {
ObjectAnimator translationX = ObjectAnimator.ofFloat(this, "translationX", mFingerPoint.x - mCaptureView.getWidth() / 2, mFixPoint.x - mCaptureView.getWidth() / 2);
ObjectAnimator translationY = ObjectAnimator.ofFloat(this, "translationY", mFingerPoint.y - mCaptureView.getHeight() / 2, mFixPoint.y - mCaptureView.getHeight() / 2);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(translationX, translationY);
animatorSet.setDuration(1000 * 2);
animatorSet.start();
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (mDragBoomViewTouchListener != null) {
mDragBoomViewTouchListener.reset();
}
}
});
}
但是实际跑的时候发现点似乎不对,具体原因还没有找到,也不想纠结太多。但是我认为可能的原因是,贝塞尔曲线绘制的原始点和拖动点都位于View的中心,而位移动画计算的时候我们使用View的左上角进行计算,因此可能存在偏差。在本篇中,最容易出问题的是各种坐标。事实上,对于本节位置相关内容,我们需要考虑以下4点
a 是否需要加上状态栏高度
b 绘制点或者图形的时候是否需要考虑View的宽高
c 进行位移动画时应该以图像的左上角为中心进行计算。
事实上本节中
mDragView.initPoints(location[0] + (mOriginView.getMeasuredWidth() / 2), location[1] + (mOriginView.getMeasuredHeight() / 2) - Utils.getStatusBarHeight(mContext));
mDragView.updatePosition(event.getRawX(), event.getRawY() - Utils.getStatusBarHeight(mContext));
两种不同的点的计算方式很容易让人产生误会。我觉得比较好的方式是记录下手指点击的坐标与View的中心相差的差值,统一在dragView中进行处理,而不是通过外部DragBoomViewTouchListener的不同标准传到DragBoomView再对坐标进行二次纠正。
5.小bug:
如图 拖动的点强制表示为View的正中心,使用第4条中的方式有可能规避这个问题。
6.在下一节视频中,使用到了TypeEvaluator,我认为在本节中的回弹动画也可以使用TypeEvaluator代替,如下所示 而不是写两个函数getPointByPercent和evaluateValue,这样写不够通用
private void playBackAnimate() {
//4.2计算回弹轨迹,添加回弹动画
final PointF start = new PointF(mFingerPoint.x, mFingerPoint.y);
final PointF end = new PointF(mFixPoint.x, mFixPoint.y);
BezierTypeEvaluator evaluator = new BezierTypeEvaluator();
ValueAnimator animator = ObjectAnimator.ofObject(evaluator,start,end);
animator.setDuration(250);
animator.addUpdateListener(animation -> {
// float percent = (float) animation.getAnimatedValue();// 0 - 1
// PointF pointF = Utils.getPointByPercent(start, end, percent);
PointF pointF = (PointF) animation.getAnimatedValue();
// 用代码更新拖拽点
updatePosition(pointF.x, pointF.y);
});
//省略其他代码
}
static class BezierTypeEvaluator implements TypeEvaluator<PointF>{
@Override
public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
float pointX = startValue.x + (endValue.x - startValue.x) * fraction;
float pointY = startValue.y + (endValue.y - startValue.y) * fraction;
return new PointF(pointX, pointY);
}
}
code:
https://github.com/caihuijian/learn_darren_android.git