每周一个小轮子之 贴纸效果

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/rikkatheworld/article/details/100097138

上周有点事情,所以拖到今天发。
这篇blog是学习 传送门的贴纸效果。

Github在这里:
Demo地址

先上个效果图:

在这里插入图片描述
是一个可以做平移、缩放、旋转、删除的一个简单的贴纸效果。

上周因为学习了关于矩阵的映射,所以在这个View里面就会做得相对轻松一些。

这周在运用到矩阵的基础上,也学会了去计算双指同时滑动时产生的向量

向量与旋转

其实做这个View比较核心的地方就是 平移、缩放和旋转了。
旋转因为之前没有触及到,所以这次学习通过 矩阵Matrix来进行旋转。
而旋转的参考系,就是 两个手指触摸点的连线 与 x轴的夹角为α,通过α的变化去改变图片的旋转系数

首先我们要创建两个向量,分别是:上次双指之间的向量,和当前双指之间的向量:

    //以PointF的形式来记录向量,其实就是触摸的点的 (x1,y1)和(x2,y2)的差值,向量是需要他们进行计算之后才能得出来的
    //记录双指之间的向量
    private PointF mCurrentVector = new PointF();
    //记录上次双指的向量
    private PointF mLastVector = new PointF();

接着就是在获取到MotionEvent的时候,对双指的操作进行数据的处理

 public void onTouch(MotionEvent event) {
        switch (event.getActionMasked()) {
            ....
            case MotionEvent.ACTION_POINTER_DOWN:
            //通过getPointCount()来判断几个手指触摸
                if (event.getPointerCount() == 2) {
                    //双指来记录两个point
                    mFirstPoint.set(event.getX(0), event.getY(0));
                    mSecondPoint.set(event.getX(1), event.getY(1));
                    //记录双指间的向量
                    mLastVector.set(mFirstPoint.x - mSecondPoint.x, mFirstPoint.y - mSecondPoint.y);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (event.getPointerCount() == 2) {
                    //记录双指点的位置
                    mFirstPoint.set(event.getX(0), event.getY(0));
                    mSecondPoint.set(event.getX(1), event.getY(1));
                    //操作旋转
                    mCurrentVector.set(mFirstPoint.x - mSecondPoint.x, mFirstPoint.y - mSecondPoint.y);
                    float rotate = calculateDegrees(mLastVector, mCurrentVector);
                    rotate(rotate);
                    mLastVector.set(mCurrentVector.x, mCurrentVector.y);
                }
                break;
        }
    }

然后我们将 之前的向量和当前的向量,通过对比可以知道 旋转角度的偏移量

    /**
     * 计算旋转度数,通过tan2()和toDegree()来计算出旋转的角度
     */
    private float calculateDegrees(PointF lastVector, PointF currentVector) {
        float lastDegrees = (float) Math.atan2(lastVector.y, lastVector.x);
        float currentDegrees = (float) Math.atan2(currentVector.y, currentVector.x);
        return (float) Math.toDegrees(currentDegrees - lastDegrees);
    }

最后通过rotate()来旋转矩阵,其实就是通过View的 矩阵的 postRotate()方法来旋转一定的度数:

    /**
     * 旋转操作
     * 使用matrix去做偏移
     * 完成偏移后还要更新 points坐标
     */
    void rotate(float degrees) {
        //以中心为轴旋转
        mMatrix.postRotate(degrees, mCenterPoint.x, mCenterPoint.y);
        updatePoints();
    }

旋转完之后,matrix发生了变化,我们要映射其四周的点,便于之后做焦点图片时,会用框 框柱它:

   private void updatePoints() {
        //更新贴纸坐标,srcPoints是最初时的包裹View的点坐标,dstPoints是matrix变换之后的点坐标
        mMatrix.mapPoints(dstPoints, srcPoints);
    }

通过这样我们就能完成旋转了。

贴纸类

在本demo中,并没有把贴纸类做成一个“View”,而是单纯做成一个类,它是通过 矩阵 去实现View的变化,它有旋转、平移、缩放的函数。
那么它的绘制是在哪里呢,它的点击事件又是怎么触发的呢?

我们把容纳贴纸的 ViewGroup做成一个View,通过给这个View加“贴纸类”,然后让这个View去绘制出每一个贴纸,并且接受每一个点击事件,通过点击事件去控制 单个“贴纸的行为”,这样,就不用考虑滑动冲突的问题了。onTouchEvent直接传true就是了。
我们来看看贴纸类:

    //没有任何接触、与操作的模式
    public static final int MODE_NONE = 0;
    //单指按下的时的状态,并且可以移动
    public static final int MODE_SINGLE = 1;
    //双指按下的状态,可以缩放大小
    public static final int MODE_POINT = 2;

    //设置一个内边距值
    private static final int PADDING = 20;

    //贴纸图像
    private Bitmap mBitmap;
    //删除图标图像
    private Bitmap mDelBitmap;
    //贴纸边界
    private RectF mBitmapBound;
    //删除图标边界
    private RectF mDelBitmapBound;
    //图像矩阵
    private Matrix mMatrix;
    //该贴纸是否获得焦点
    private boolean isFocus;
    //bitmap的中心点
    private PointF mCenterPoint;
    //上次双指移动的距离
    private float mLastDoubleDistance;
    //上次触摸的点
    private PointF mLastPoint = new PointF();
    //双指触控下 当前触摸的点1
    private PointF mFirstPoint = new PointF();
    //双指触控下 当前触摸的点2
    private PointF mSecondPoint = new PointF();
    //记录矩阵的点坐标,矩阵变换后也要变更
    private float[] srcPoints;
    //因为矩阵变化后 原坐标也会变化,无法变回原来的,所以需要记录一个目标的矩阵
    private float[] dstPoints;
    //记录双指之间的向量
    private PointF mCurrentVector = new PointF();
    //记录上次双指的向量
    private PointF mLastVector = new PointF();
    //当前模式
    private int mMode;

构造时,需要确定 这个贴纸的 边框,即srcPoints,它便于我们画边框,并且要构造删除图标:

    public RikkaStickerView(Context context, Bitmap bitmap) {
        mMatrix = new Matrix();
        mCenterPoint = new PointF();
        this.mBitmap = bitmap;

        mBitmapBound = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());
        srcPoints = new float[]{
                //左上
                0, 0,
                //右上
                bitmap.getWidth(), 0,
                //左下
                0, bitmap.getHeight(),
                //右下
                bitmap.getWidth(), bitmap.getHeight(),
                //中点
                bitmap.getWidth() / 2f, bitmap.getHeight() / 2f
        };
        dstPoints = srcPoints.clone();

        //创建删除图标并定义边界,加上padding
        mDelBitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.icon_delete);
        mDelBitmapBound = new RectF(0 - mDelBitmap.getWidth() / 2 - PADDING, 0 - mDelBitmap.getHeight() / 2 - PADDING,
                mDelBitmap.getWidth() / 2f + PADDING, mDelBitmap.getHeight() / 2f + PADDING);

        //将贴纸默认放在屏幕中间
        WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics displayMetrics = new DisplayMetrics();
        manager.getDefaultDisplay().getMetrics(displayMetrics);
        float dx = displayMetrics.widthPixels / 2f - mBitmap.getWidth() / 2f;
        float dy = displayMetrics.heightPixels / 2f - mBitmap.getHeight() / 2f;
        translate(dx, dy);
    }

接下来是三个操作:

/**
     * 平移操作,偏移量为dx,dy
     * 使用matrix去做偏移
     * 完成偏移后还要更新 points坐标
     */
    void translate(float dx, float dy) {
        mMatrix.postTranslate(dx, dy);
        updatePoints();
    }

    /**
     * 缩放操作,跟平移操作一样
     */
    void scale(float scale) {
        //以View的中点为轴放大
        mMatrix.postScale(scale, scale, mCenterPoint.x, mCenterPoint.y);
        updatePoints();
    }

    /**
     * 旋转操作
     */
    void rotate(float degrees) {
        //以中心为轴旋转
        mMatrix.postRotate(degrees, mCenterPoint.x, mCenterPoint.y);
        updatePoints();
    }

    private void updatePoints() {
        //更新贴纸坐标
        mMatrix.mapPoints(dstPoints, srcPoints);
        // 更新贴纸中心坐标
        mCenterPoint.set(dstPoints[8], dstPoints[9]);
    }

接下来就是绘制,它自己本身当然没有绘制函数,也没有画布,所以这里的onDraw方法是由父View来调用的,Canvas也是父View传过来的:

 /**
     * 绘制贴纸本身,这个方法需要父View去调用
     * Canvas 是父View的canvas,paint也是
     */
    public void onDraw(Canvas canvas, Paint paint) {
        //绘制贴纸,带上matrix参数
        canvas.drawBitmap(mBitmap, mMatrix, paint);

        //如果该贴纸是被选中的目标,则要绘制其边框,以及移除按钮
        if (isFocus) {
            //画点要用points画,因为图片会变化,所以必须要有记录号的点,所以points在这里就派上用场了
            canvas.drawLine(dstPoints[0] - PADDING, dstPoints[1] - PADDING, dstPoints[2] + PADDING, dstPoints[3] - PADDING, paint);
            canvas.drawLine(dstPoints[2] + PADDING, dstPoints[3] - PADDING, dstPoints[6] + PADDING, dstPoints[7] + PADDING, paint);
            canvas.drawLine(dstPoints[6] + PADDING, dstPoints[7] + PADDING, dstPoints[4] - PADDING, dstPoints[5] + PADDING, paint);
            canvas.drawLine(dstPoints[4] - PADDING, dstPoints[5] + PADDING, dstPoints[0] - PADDING, dstPoints[1] - PADDING, paint);
            //绘制删除按钮
            canvas.drawBitmap(mDelBitmap, dstPoints[0] - mDelBitmap.getWidth() / 2f - PADDING, dstPoints[1] - mDelBitmap.getHeight() / 2f - PADDING, paint);
        }
    }

接下来是onTouchEvent方法,当然本身他本身也是不会调用的,所以TouchEvent是由父View传下来的:

   /**
     * 自己定义onTouch方法,根据父View传的event来做各种操作
     * 既然走到这个方法了,那么就说明 已经触摸到贴纸了
     */
    public void onTouch(MotionEvent event) {
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                mMode = MODE_SINGLE;
                //记录按下的位置
                mLastPoint.set(event.getX(), event.getY());
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                if (event.getPointerCount() == 2) {
                    mMode = MODE_POINT;
                    //双指来记录两个point
                    mFirstPoint.set(event.getX(0), event.getY(0));
                    mSecondPoint.set(event.getX(1), event.getY(1));
                    //计算双指之间的距离
                    mLastDoubleDistance = calculateDistance(mFirstPoint, mSecondPoint);
                    //记录双指间的向量
                    mLastVector.set(mFirstPoint.x - mSecondPoint.x, mFirstPoint.y - mSecondPoint.y);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                //通过模式来确定行为
                if (mMode == MODE_SINGLE) {
                    //如果是单指拖动,则移动到指定的位置
                    translate(event.getX() - mLastPoint.x, event.getY() - mLastPoint.y);
                    mLastPoint.set(event.getX(), event.getY());
                }
                if (mMode == MODE_POINT && event.getPointerCount() == 2) {
                    //记录双指点的位置
                    mFirstPoint.set(event.getX(0), event.getY(0));
                    mSecondPoint.set(event.getX(1), event.getY(1));
                    //操作自由缩放
                    float distance = calculateDistance(mFirstPoint, mSecondPoint);
                    //根据双指移动的距离获取缩放系数
                    float scale = distance / mLastDoubleDistance;
                    scale(scale);
                    mLastDoubleDistance = distance;
                    //操作旋转
                    mCurrentVector.set(mFirstPoint.x - mSecondPoint.x, mFirstPoint.y - mSecondPoint.y);
                    float rotate = calculateDegrees(mLastVector, mCurrentVector);
                    rotate(rotate);
                    mLastVector.set(mCurrentVector.x, mCurrentVector.y);
                }
                break;
            case MotionEvent.ACTION_UP:
                reset();
                break;
        }
    }

    /**
     * 通过两个坐标来计算他们的距离
     */
    private float calculateDistance(PointF mFirstPoint, PointF mSecondPoint) {
        float x = mFirstPoint.x - mSecondPoint.x;
        float y = mFirstPoint.y - mSecondPoint.y;
        return (float) Math.sqrt(x * x + y * y);
    }

贴纸容器View

我们需要一个容器去放这些贴纸,所以容器必须继承自View或者ViewGroup,我这里就继承自View。
它最重要的就是做两件事情:

  1. 绘制贴纸
  2. 给贴纸传递点击事件

方法如下:

    /**
     * 点击事件,处理单指移动,双指缩放
     * 单指单击删除
     */
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_POINTER_DOWN:
                //先判断是否是点到删除按钮了
                stickerView = RikkaStickerManager.getInstance().getDelButton(event.getX(), event.getY());
                if (stickerView != null) {
                    removeSticker(stickerView);
                    return true;
                }
                //再判断是否摸到某一个贴纸
                stickerView = RikkaStickerManager.getInstance().getSticker(event.getX(), event.getY());
                if (stickerView == null) {
                    //当不是单指的时候,可能会存在第二个手指摸到了贴纸(先按下两个,抬起一个的情况)
                    if (event.getPointerCount() == 2) {
                        stickerView = RikkaStickerManager.getInstance().getSticker(event.getX(1), event.getY(1));
                    }
                }
                if (stickerView != null) {
                    RikkaStickerManager.getInstance().setFocusSticker(stickerView);
                }
                break;
            default:
                break;
        }
        if (stickerView != null) {
            stickerView.onTouch(event);
        } else {
            //如果没有点击到,则取消所有贴纸的焦点
            RikkaStickerManager.getInstance().clearAllFocus();
        }
        invalidate();
        return true;
    }

    /**
     * 绘制所有的子贴纸,并且根据是否被选中来画
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        List<RikkaStickerView> stickerViews = RikkaStickerManager.getInstance().getmStickerList();
        for (int i = 0; i < stickerViews.size(); i++) {
            RikkaStickerView stickerView = stickerViews.get(i);
            stickerView.onDraw(canvas, mPaint);
        }
    }

贴纸管理类

看到上面的代码,还有一个专门用来管理 贴纸的Manager单例类,
它其实也很简单,就是维护一个 贴纸列表,每次传入一个坐标,就判断这个坐标在不在 这些贴纸里面。
而这个方法,在上周的 坐标映射里面就已经学会了:

/**
 * 贴纸管理类,使用单例模式
 * 对每个“贴纸”进行保存、增加、删除
 */
public class RikkaStickerManager {
    private static final String TAG = "RikkaStickerManager";

    public static RikkaStickerManager instance;

    //贴纸List,统一进行管理
    private List<RikkaStickerView> mStickerList = new ArrayList<>();

    public static RikkaStickerManager getInstance() {
        if (instance == null) {
            synchronized (RikkaStickerManager.class) {
                if (instance == null) {
                    instance = new RikkaStickerManager();
                }
            }
        }
        return instance;
    }

    /**
     * 添加贴纸,就是往List里面添加
     */
    void addSticker(RikkaStickerView stickerView) {
        mStickerList.add(stickerView);
    }

    /**
     * 移除指定的贴纸
     */
    void removeSticker(RikkaStickerView stickerView) {
        Bitmap bitmap = stickerView.getmBitmap();
        if (bitmap != null && !bitmap.isRecycled()) {
            //即使回收
            bitmap.recycle();
        }
        mStickerList.remove(stickerView);
    }

    /**
     * 移除所有贴纸
     */
    void removeAllSticker() {
        for (int i = 0; i < mStickerList.size(); i++) {
            Bitmap bitmap = mStickerList.get(i).getmBitmap();
            if (bitmap != null && !bitmap.isRecycled()) {
                bitmap.recycle();
            }
        }
        mStickerList.clear();
    }

    /**
     * 设置当前贴纸为选中(焦点)贴纸
     */
    void setFocusSticker(RikkaStickerView focusSticker) {
        for (int i = 0; i < mStickerList.size(); i++) {
            RikkaStickerView sticker = mStickerList.get(i);
            if (sticker == focusSticker) {
                sticker.setFocus(true);
            } else {
                sticker.setFocus(false);
            }
        }
    }

    /**
     * 全部设为没有焦点
     */
    void clearAllFocus() {
        for (int i = 0; i < mStickerList.size(); i++) {
            RikkaStickerView stickerView = mStickerList.get(i);
            stickerView.setFocus(false);
        }
    }

    /**
     * 根据触摸的点来返回触摸的贴纸
     */
    RikkaStickerView getSticker(float x, float y) {
        for (int i = mStickerList.size() - 1; i >= 0; i--) {
            RikkaStickerView sticker = mStickerList.get(i);
            //因为points 映射之后都会改变所以必须每次都重置
            float[] points = new float[]{x, y};
            //根据invert来做转换
            Matrix matrix = new Matrix();
            sticker.getmMatrix().invert(matrix);
            matrix.mapPoints(points);
            //根据边界来判断 点是否在该View中
            if (sticker.getmBitmapBound().contains(points[0], points[1])) {
                return sticker;
            }
        }
        return null;
    }

    /**
     * 根据触摸的点来判断是否点击到删除按钮,如果点到就会删除
     */
    RikkaStickerView getDelButton(float x, float y) {
        for (int i = mStickerList.size() - 1; i >= 0; i--) {
            RikkaStickerView sticker = mStickerList.get(i);
            float[] points = new float[]{x, y};
            //根据invert来做转换
            Matrix matrix = new Matrix();
            sticker.getmMatrix().invert(matrix);
            matrix.mapPoints(points);
            //根据边界来判断 点是否在该View中
            if (sticker.getmDelBitmapBound().contains(points[0], points[1])) {
                return sticker;
            }
        }
        return null;
    }

    public List<RikkaStickerView> getmStickerList() {
        return mStickerList;
    }
}

Ok,到这里小轮子就做完了,因为上周的拖到了这周了,所以这周还要再多学一个。
不过反正AndroidBus和玩Android的轮子那么多,也不缺着学习哈哈哈哈。

猜你喜欢

转载自blog.csdn.net/rikkatheworld/article/details/100097138