红橙Darren视频笔记 任意控件实现拖动消失爆炸效果

效果
在这里插入图片描述
注意 本节内容基于上一篇博客的代码继续编写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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值