(1) 深度解析微信编辑图片功能 - 细节分析

学习微信编辑功能可以让我们更加扎实Android自定义的基础,学习最好的方法的就是
死命啃源码,当然了,我们弄不到微信的源码,但是我们可以找到别人写好的源码来一个一个学习相关功能,找到一个代码写的很好的一个源码,而且本身也是仿照微信功能的,我们看下源码地址:
minetsh/Imaging: Android Image Edit Lib. Android 图片编辑库,微信图片编辑库 (github.com)
作者针对该源码写了一份博客,有兴趣的同学可以去看下
Android 图片编辑的原理与实现——涂鸦与马赛克 (qq.com)

如果同学们觉得还了解不够多的话,那么我们一起全面解析该源码吧!
我们简称 minetsh/Imaging: Android Image Edit Lib. Android 图片编辑库,微信图片编辑库 (github.com) 为 图片编辑库

我为了更加彻底学习源码,将一个一个功能全部拆解出来一个例子,这样理解会容易很多!
在最下面会放出学习的例子源码。
但是在我们讲解例子前,先讲图片编辑库主要由两个类来构成,一个是自定义类IMGView,继承于FrameLayout控件,然后在绘制图片的时候,会通过自定义一个类IMGImage来进行相关绘制。所有关于触摸操作(单点移动、多点缩放、涂鸦、马赛克等)都是在IMGView的onTouchEvent下解析,针对当前模式解析触摸操作,进行对应的绘制,这就是编辑图片的主要思路了。

那么我们看下图片编辑库有以下几大技术点:

  • Matrix矩阵
  • 移动图片
  • 缩放图片
  • 涂鸦图片
  • 马赛克图片
  • 裁剪图片

Matrix矩阵

所有有关变换图片的操作都会涉及到Matrix,所以我们这边单独针对Matrix矩阵进行详细讲解,我单独抽出一个文章讲解了Matrix
Android Matrix的set\pre\post方法的区别和使用

移动图片

  • 通过GestureDetector手势监听类,传递MotionEvent来执行相关onScroll方法
  • 通过GestureDetector传递的xy,在当前view的xy基础上叠加
  • 最后得到的值,通过当前view的scrollTo方法调用
  • 图像的平移不会影响图片的画布位置,当前控件的视图窗口会发生变化,也就是scrollX、scrollY 的值发生变化。
    private void initialize(Context context) {
        // 手势监听类
        mGDetector = new GestureDetector(context, new MoveAdapter());
    }

    /**
     * 处理触屏事件
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return onTouch(event);
    }

    /**
     * 处理触屏事件.详情
     */
    boolean onTouch(MotionEvent event) {
        Log.d(TAG, "onTouch");
        return onTouchNONE(event);
    }

    private boolean onTouchNONE(MotionEvent event) {
        Log.d(TAG, "onTouchNONE");
        return mGDetector.onTouchEvent(event);
    }

    private boolean onScroll(float dx, float dy) {
        return onScrollTo(getScrollX() + Math.round(dx), getScrollY() + Math.round(dy));
    }

    private boolean onScrollTo(int x, int y) {
        if (getScrollX() != x || getScrollY() != y) {
            scrollTo(x, y);
            return true;
        }
        return false;
    }

    private class MoveAdapter extends GestureDetector.SimpleOnGestureListener {

        @Override
        public boolean onDown(MotionEvent e) {
            return true;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            return ScrollyFrameLayout.this.onScroll(distanceX, distanceY);
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            // TODO
            return super.onFling(e1, e2, velocityX, velocityY);
        }
    }

缩放图片

跟移动类似,通过ScaleGestureDetector类触发onScale,在本身view的基础上添加getFocusX

    private void initialize(Context context) {
        // 用于处理缩放的工具类
        mSGDetector = new ScaleGestureDetector(context, this);
    }

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        if (mPointerCount > 1) {
            mImage.onScale(detector.getScaleFactor(),
                    getScrollX() + detector.getFocusX(),
                    getScrollY() + detector.getFocusY());
            invalidate();
            return true;
        }
        return false;
    }

可以看到mImage.onScale()的具体代码如下

    public void onScale(float factor, float focusX, float focusY) {
        if (factor == 1f) return;

        // 针对这个有图片的边框进行比例缩放
        M.setScale(factor, factor, focusX, focusY);
        M.mapRect(mFrame);
    }

M是Matrix矩阵,关于这个Matrix.setScale就有点意思了,具体详解如下:
public boolean postScale(float sx, float sy, float px, float py)
这个api的第一个参数是X轴的缩放大小,第二个参数是Y轴的缩放大小,第三四个参数是缩放中心点。
一般这个缩放中心点比较不好理解。这个中心点并不一定在图片的中心位置。有可能在图片的外面。我们可以这样理解。以这个中心点为坐标原点画X轴跟Y轴。图片可能会跟X轴或者Y轴相交,也可能完全在一个区间内。
画图理解如下:

image.png

那么用到这个方法就能实现到 客户根据当前触摸焦点而进行缩放

涂鸦图片

涂鸦图片也是在onTouchEvent分发事件下执行相关涂鸦操作

    /**
     * 处理触屏事件
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return onTouch(event);
    }

    /**
     * 处理触屏事件.详情
     */
    boolean onTouch(MotionEvent event) {
        return onTouchPath(event);
    }

    /**
     * 画笔线
     */
    private boolean onTouchPath(MotionEvent event) {
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                // 钢笔初始化
                return onPathBegin(event);
            case MotionEvent.ACTION_MOVE:
                // 画线
                return onPathMove(event);
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                // 画线完成,绘制路径加入到绘制列表
                return mPen.isIdentity(event.getPointerId(0)) && onPathDone();
        }
        return false;
    }

关于绘制整体思路是:通过上面代码分发绘制事件,onPathMove执行画线,每次画线都会invalidate view,当画线完成后,绘制路径加入到绘制列表,在onDraw事件里面,都会重新把绘制路径全部渲染出来。
比较有点意思的一个技术点是,在缩放view的情况下,如何精确到绘制路径呢,那么就必须仔细看看IMGImage的addPath方法了,具体看代码注释和图解

    /**
     * addPath方法详解:
     * M.setTranslate(sx, sy);
     * 矩阵平移到跟view的xy轴一样,注意,是getScrollX()和getScrolly()
     *
     * M.postTranslate(-mFrame.left, -mFrame.top);
     * 如果按照getScrollX()直接绘制进手机屏幕上是会出格的,因为view能缩放到比手机屏幕还要大,那么就需要减掉mFrame的x和y,剩下的就是手机绘制的正确的点
     */
    public void addPath(IMGPath path, float sx, float sy) {
        if (path == null) return;

        float scale = 1f / getScale();
        M.setTranslate(sx, sy);
        M.postTranslate(-mFrame.left, -mFrame.top);
        M.postScale(scale, scale);
        // 矩阵变换
        path.transform(M);

        mDoodles.add(path);
    }

    /**
     * 1 * view缩放后的宽度 / 图片固定宽度 = 缩放比例
     */
    public float getScale() {
        return 1f * mFrame.width() / mImage.getWidth();
    }
图解

如果觉得还不够理解,具体还请到源码作者博客看看Android 图片编辑的原理与实现——涂鸦与马赛克 (qq.com)

马赛克

在马赛克这里频繁用到一个很有意思的东西,canvas.save() 和 canvas.restore(),restoreToCount跟他们一样意思,只是restoreToCount细化到某个id。
如果不是很了解他们的意思,可以直接看
Android canvas.save()与canvas.restore()的使用总结_Nothing-CSDN博客

马赛克跟涂鸦一样,都是通过onTouchEvent分发进行绘画动作,但是跟涂鸦不一样的是,马赛克会先画一个马赛克的图片,按照源码作者的原话是:

其实很简单,马赛克就是将整个区域的颜色变成一个颜色值,如将 10x10 区域内的颜色变成其中的一个颜色值,所以我们将一张图片缩放到一个较小的尺寸,然后再放大到原始尺寸去显示,这个图片就很模糊了,然后关闭 Paint 的滤波功能:paint.setFilterBitmap(false),这样就得到了一个图片的马赛克效果,如下:
绘制马赛克图片
绘制马赛克图片如下:
    /**
     * 创建同样的马赛克图和马赛克画笔
     */
    private void makeMosaicBitmap() {
        Log.d(TAG, "makeMosaicBitmap");
        if (mMosaicImage != null || mImage == null) {
            return;
        }

        // 原图的宽高相除64
        int w = Math.round(mImage.getWidth() / 64f);
        int h = Math.round(mImage.getHeight() / 64f);

        // 取最大值,即不能小于8
        w = Math.max(w, 8);
        h = Math.max(h, 8);

        // 马赛克画刷,注意是SRC_IN,刷子刷后就显示相应的马赛克层了
        if (mMosaicPaint == null) {
            mMosaicPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            mMosaicPaint.setFilterBitmap(false);
            mMosaicPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        }

        // 创建马赛克图
        mMosaicImage = Bitmap.createScaledBitmap(mImage, w, h, false);
    }

所以为什么要将一整张图变成马赛克呢?可以用一张图简单表示如下:

这样就能绘制马赛克

将马赛克路径图层与马赛克图层合并显示即可。也就是 Paint 的如下功能:

paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN))
关键代码如下:

    private void onDrawImages(Canvas canvas) {
        // 图片
        mImage.onDrawImage(canvas);

        // 马赛克
        if (!mImage.isMosaicEmpty() || (!mPen.isEmpty())) {
            int count = mImage.onDrawMosaicsPath(canvas);

            mDoodlePaint.setStrokeWidth(IMGPath.BASE_MOSAIC_WIDTH);
            canvas.save();
            canvas.translate(getScrollX(), getScrollY());
            canvas.drawPath(mPen.getPath(), mDoodlePaint);
            canvas.restore();

            mImage.onDrawMosaic(canvas, count);
            canvas.save();
            canvas.restore();
        }
    }
    /**
     * 绘制马赛克路径
     */
    public int onDrawMosaicsPath(Canvas canvas) {
        Log.d(TAG, "onDrawMosaicsPath");
        // 所有状态都保存
        int layerCount = canvas.saveLayer(mFrame, null, Canvas.ALL_SAVE_FLAG);

        if (!isMosaicEmpty()) {
            canvas.save();
            float scale = getScale();
            canvas.translate(mFrame.left, mFrame.top);
            canvas.scale(scale, scale);
            for (IMGPath path : mMosaics) {
                path.onDrawMosaic(canvas, mPaint);
            }
            canvas.restore();
        }

        return layerCount;
    }
    /**
     * 绘制马赛克
     */
    public void onDrawMosaic(Canvas canvas, int layerCount) {
        Log.d(TAG, "onDrawMosaic");
        canvas.drawBitmap(mMosaicImage, null, mFrame, mMosaicPaint);
        canvas.restoreToCount(layerCount);
    }

最后放出源码:zhongjhATC/ImageingStudy: 这是详细拆分Imaging项目的代码进行一个一个的学习代码逻辑 (github.com)

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

推荐阅读更多精彩内容