Android 贴纸

概述

了解过自定义View的童鞋 对Canvas.drawBitmap(Bitmap, Matrix, Paint)这个函数应该不会陌生,Bitmap的位置、大小、旋转角度、扭曲程度等都由Matrix来管理,而实现贴纸效果的就需要借助这个神奇的函数。我们可以通过很多种方法获取到贴纸的Bitmap,也可以很容易的定义绘制Bitmap所使用的Paint,那么剩下我们只需要关心怎样可以借助Matrix来让贴纸随着我们的指尖翩翩起舞。


device-2016-04-07-204711~1.gif

为了更好的管理每个贴纸的Bitmap和Matrix信息,我简单的将二者进行了封装,大家不要在意名字,知道这个类是干嘛的就好了,毕竟如何起一个优雅准确的名字是一个世界性的难题。

public static class ImageGroup {
    public Bitmap bitmap;
    public Matrix matrix = new Matrix();

    //删除贴纸时释放资源时使用
    public void release() {
        if (bitmap != null) {
            bitmap.recycle();
            bitmap = null;
        }

        if (matrix != null) {
            matrix.reset();
            matrix = null;
        }
    }
}

说到随着指尖,我们就会想到Android丰富的手势操作,因为是自定义View,所有对Bitmap的操作都需要用到手势触点坐标,因此我使用了View的onTouchEvent(MotionEvent event)方法直接对手势触点就行操作,onTouchEvent也是整个贴纸模块的核心。

public boolean onTouchEvent(MotionEvent event) {
    switch (event.getActionMasked()) {
        case MotionEvent.ACTION_DOWN:
            anchorX = event.getX();
            anchorY = event.getY();
            moveTag = decalCheck(anchorX, anchorY);
            deleteTag = deleteCheck(anchorX, anchorY);
            if (moveTag != -1 && deleteTag == -1) {
                downMatrix.set(mDecalImageGroupList.get(moveTag).matrix);
                mode = DRAG;
            }
            break;

        case MotionEvent.ACTION_POINTER_DOWN:
            moveTag = decalCheck(event.getX(0), event.getY(0));
            transformTag = decalCheck(event.getX(1), event.getY(1));
            if (moveTag != -1 && transformTag == moveTag && deleteTag == -1) {
                downMatrix.set(mDecalImageGroupList.get(moveTag).matrix);
                mode = ZOOM;
            }
            oldDistance = getDistance(event);
            oldRotation = getRotation(event);
            midPoint = midPoint(event);
            break;

        case MotionEvent.ACTION_MOVE:
            if (mode == ZOOM) {
                moveMatrix.set(downMatrix);
                float newRotation = getRotation(event) - oldRotation;
                float newDistance = getDistance(event);
                float scale = newDistance / oldDistance;
                moveMatrix.postScale(scale, scale, midPoint.x, midPoint.y);// 缩放
                moveMatrix.postRotate(newRotation, midPoint.x, midPoint.y);// 旋转
                if (moveTag != -1) {
                    mDecalImageGroupList.get(moveTag).matrix.set(moveMatrix);
                }
                invalidate();
            } else if (mode == DRAG) {
                moveMatrix.set(downMatrix);
                moveMatrix.postTranslate(event.getX() - anchorX, event.getY() - anchorY);// 平移
                if (moveTag != -1) {
                    mDecalImageGroupList.get(moveTag).matrix.set(moveMatrix);
                }
                invalidate();
            }
            break;

        case MotionEvent.ACTION_UP:
            if (deleteTag != -1) {
                mDecalImageGroupList.remove(deleteTag).release();
                invalidate();
            }
            mode = NONE;
            break;

        case MotionEvent.ACTION_POINTER_UP:
            mode = NONE;
            break;
    }
    return true;
}

手势操作

onTouchEvent中我们使用了ACTION_DOWN、ACTION_POINTER_DOWN、ACTION_MOVE、ACTION_UP、ACTION_POINTER_UP五种事件,其中ACTION_DOWN和ACTION_UP、ACTION_POINTER_DOWN和ACTION_POINTER_UP分别对应。

  • ACTION_DOWN和ACTION_UP:当View从无到有手指触摸,ACTION_DOWN会被触发,对应的ACTION_UP则为从有到无,也就是说只有当没有任何一根手指在触摸View时ACTION_UP才会被触发,ACTION_DOWN和ACTION_UP是整个手势操作生命周期的起点和终点。
  • ACTION_POINTER_DOWN和ACTION_POINTER_UP:当View有多点触摸时ACTION_POINTER_DOWN会被触发,而当其中某个触点消失后ACTION_POINTER_UP会被触发。这里我们只考虑有两根手指触摸的情况。
  • ACTION_MOVE:当View被触摸且该触摸点在移动时ACTION_MOVE会被触发,多点触摸时无论哪个点移动都会触发。

Matrix的Translate(平移)外,Scale(缩放)、Rotate(旋转)、Skew(扭曲)四大操作除了Skew外我们都需要使用,对应到手势上我们通过单点触摸来控制Bitmap平移,通过多点触摸来控制Bitmap缩放和旋转,因此,在ACTION_MOVE阶段我们需要根据两种不同情况做区分。

int NONE = 0;//无
int DRAG = 1;//平移模式
int ZOOM = 2;//缩放、旋转模式

我们定义三种mode,mode的初始值为NONE,当ACTION_DOWN被触发时mode置为DRAG,当ACTION_POINTER_DOWN被触发时mode置为ZOOM。当ACTION_MOVE被触发时,我们对mode进行判断,针对DRAG和ZOOM两种情况分别进行处理。

  1. mode == DRAG
anchorX = event.getX();
anchorY = event.getY();
moveTag = decalCheck(anchorX, anchorY);
deleteTag = deleteCheck(anchorX, anchorY);
if (moveTag != -1 && deleteTag == -1) {
    downMatrix.set(mDecalImageGroupList.get(moveTag).matrix);
    mode = DRAG;
}

ACTION_DOWN被触发时我们首先将当前触摸点的坐标保存下来以备使用。之后要判断当前触摸点是否在某一个贴纸的Bitmap范围内以及当前触摸点是否在某一个贴纸的删除按钮范围内,我们分别用moveTag和deleteTag来保存结果,当结果为-1时表示没有在任何相关范围内,结果为0~贴纸数量-1时表示在某个贴纸的相关范围内。只有当moveTag != -1 && deleteTag == -1(触摸点某一个贴纸范围内且不在任何删除按钮范围内)时,我们将当前贴纸的Matrix保存到downMatrix中并将mode置成DRAG,若deleteTag不等于-1时,我们在ACTION_UP就将对应的贴纸从贴纸列表中移除。

moveMatrix.set(downMatrix);
moveMatrix.postTranslate(event.getX() - anchorX, event.getY() - anchorY);// 平移
if (moveTag != -1) {
    mDecalImageGroupList.get(moveTag).matrix.set(moveMatrix);
}
invalidate();

进入到ACTION_MOVE阶段,我们首先将downMatrix赋值给moveMatrix,downMatrix是这次手势操作的起始Matrix,之后的变换都是基于downMatrix进行的,所以我们不能直接对downMatrix进行操作,moveMatrix承担了这个责任。DRAG模式下表示当前要进行的是平移操作,而平移的横纵距离是在ACTION_DOWN阶段保存下来的触摸点横纵坐标值与当前移动到的触摸点横纵坐标值的差值。最后将处理好的moveMatrix赋值回该贴纸的Matrix,并调用重绘函数,完成此次贴纸平移操作。因为ACTION_MOVE会在ACTION_UP触发之前一直保持,所以整个平移操作会一直持续,贴纸随着手指移动而移动的效果就出现了。

  1. mode == ZOOM
moveTag = decalCheck(event.getX(0), event.getY(0));
transformTag = decalCheck(event.getX(1), event.getY(1));
if (moveTag != -1 && transformTag == moveTag && deleteTag == -1) {
    downMatrix.set(mDecalImageGroupList.get(moveTag).matrix);
    mode = ZOOM;
}
oldDistance = getDistance(event);
oldRotation = getRotation(event);
midPoint = midPoint(event);

ACTION_POINTER_DOWN被触发时,我们首先对两个触摸点是否在某个贴纸范围内进行判断,结果分别用moveTag和transformTag进行保存。当moveTag != -1 && transformTag == moveTag && deleteTag == -1(两个触摸点在同一个贴纸范围内且不在任何删除按钮范围内)条件满足时,我们将当前贴纸的Matrix保存到downMatrix中并将mode置成ZOOM。同时我们需要将当前两个触摸点之间的距离、中点、角度用oldDistance、midPoint、oldRotation保存起来以备使用。

moveMatrix.set(downMatrix);
float newRotation = getRotation(event) - oldRotation;
float newDistance = getDistance(event);
float scale = newDistance / oldDistance;
moveMatrix.postScale(scale, scale, midPoint.x, midPoint.y);// 缩放
moveMatrix.postRotate(newRotation, midPoint.x, midPoint.y);// 旋转
if (moveTag != -1) {
    mDecalImageGroupList.get(moveTag).matrix.set(moveMatrix);
}
invalidate();

进入到ACTION_MOVE阶段,我们首先将downMatrix赋值给moveMatrix。用当前的两个触摸点算出新的角度,同之前保存的值算出差值newRotation,算出新的距离newDistance并和oldDistance做商,算出缩放比例。分别对moveMatrix进行缩放和旋转操作,处理好后将moveMatrix赋值回该贴纸的Matrix,并调用重绘函数,完成此次贴纸平移操作。因为ACTION_MOVE会在ACTION_UP触发之前一直保持且在ACTION_POINTER_UP触发之前ZOOM模式会一直保持,所以缩放、旋转操作会一直持续,贴纸随着两根手指之间距离变化而变化,角度变化而变化的效果就出现了。

触摸点判断

protected float[] getBitmapPoints(Bitmap bitmap, Matrix matrix) {
    float[] dst = new float[8];
    float[] src = new float[]{
            0, 0,
            bitmap.getWidth(), 0,
            0, bitmap.getHeight(),
            bitmap.getWidth(), bitmap.getHeight()
    };
    matrix.mapPoints(dst, src);
    return dst;
}
private boolean pointCheck(ImageGroup imageGroup, float x, float y) {
    float[] points = getBitmapPoints(imageGroup);
    float x1 = points[0];
    float y1 = points[1];
    float x2 = points[2];
    float y2 = points[3];
    float x3 = points[4];
    float y3 = points[5];
    float x4 = points[6];
    float y4 = points[7];
    float edge = (float) Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
    if ((2 + Math.sqrt(2)) * edge >= Math.sqrt(Math.pow(x - x1, 2) + Math.pow(y - y1, 2))
            + Math.sqrt(Math.pow(x - x2, 2) + Math.pow(y - y2, 2))
            + Math.sqrt(Math.pow(x - x3, 2) + Math.pow(y - y3, 2))
            + Math.sqrt(Math.pow(x - x4, 2) + Math.pow(y - y4, 2))) {
        return true;
    }
    return false;
}
private boolean circleCheck(ImageGroup imageGroup, float x, float y) {
    float[] points = getBitmapPoints(imageGroup);
    float x2 = points[2];
    float y2 = points[3];
    int checkDis = (int) Math.sqrt(Math.pow(x - x2, 2) + Math.pow(y - y2, 2));
    if (checkDis < 40) {
        return true;
    }
    return false;
}

在整个手势操作流程中我们需要多次使用触摸点判断,不论是判断是否在贴纸范围还是删除按钮范围。Matrix提供了map开头的映射方法,其中的mapPoints(float[] dst, float[] src)可以将src坐标数组根据Matrix映射到dst。使用Matrix来存储Bitmap的绘制信息,Bitmap默认的绘制起点(左上角点)为(0,0),因此默认情况下Bitmap四个点的坐标为(0,0)、(bitmap.getWidth(), 0)、(0, bitmap.getHeight())、(bitmap.getWidth(), bitmap.getHeight()),依此我们可以映射出当前Bitmap四个点的坐标。我们的贴纸在整个流程任何操作下都是正方形,因此我们可以使用已知正方形四个顶点来判断第五点是否在正方形范围内,算法是第五点到四顶点的距离是否小于等于2√2倍的边长。这个算法对长方形适不适用我没有验证,如果要添加非正方形Bitmap的话需要自行优化此处。判断点是否在一个圆的范围内很简单,只要将该点到圆心的距离和半径进行比较即可。

总结

整个Android贴纸的简单实现思路就行这样,完整代码链接如下,有需要的童鞋可以搞下来看看,有什么问题或者好点子欢迎交流。
代码地址:https://github.com/JunyiZhou/AndroidExercises/tree/master/ImageHandleDemo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容