Android之圆形头像裁切

PS:今天项目测试组发现,百度地图定位的数据坐标位置是正确的,但是显示的数据是错误的.最后查来查去发现,那个商厦在百度地图上根本就没有那条数据,这让我如何显示,当初就推崇使用高德地图定位,上面的数据量比较的完整,而且定位的也比较的精准,非得用百度地图定位,这下定位不到数据,懵逼了吧..

 

学习内容:

1.自定义View+Canvas+XferMode实现圆形头像裁切

 

  头像裁切现如今在很多应用中都会得到使用,一般都是在个人资料页面设置头像,然后选择图片,或者是直接开启相机拍摄一张图片,通过裁切和缩放的手段最后显示在ImageView上就可以了.不过无论怎样裁切其实最后裁切出来仍然是一个方形的图片,因此我们需要自定义ImageView,将ImageView定义成我们想要的形状,然后将裁切到的图片显示到ImageView上就可以了.这里的ImageView我是使用的第三方框架,因为自己还没有打算自定义ImageView这块.因此就直接用了别人的东西.

 

i.Canvas的saveLayer(),restore()方法.

  实现头像裁切需要使用几种技术,首先就需要Canvas的支持,首先说一下他的结构组成,这样更加方便理解.

 

  Canvas的结构基本是这样的,在View绘制到屏幕上的时候在OnDraw()方法在调用的时候,所有的控件就会通过Paint绘制到Canvas上.其实就是画到画布上,默认情况下,我们可以把Canvas也看作成一个Layer,当我们在saveLayer()的时候,就表示我们开启一个新的图层,所有绘制的内容都会在当前图层完成,不会影响到前一张图层,相当于图层的覆盖,当我们调用restore()方法的时候,那么当前图层出栈,将所有的内容绘制到被覆盖的图层.简单的说说saveLayer()方法如何使用.

 这里我们在Canvas上先画了一个红色的圆圈,然后又入栈一个带有透明度的Layer,在当前这个Layer画一个蓝色的圆圈.

package com.example.totem.canvastest.activity;

import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.View;


public class LayersActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new SimpleView(this));
    }

    private static class SimpleView extends View {
        private static final int LAYER_FLAGS = Canvas.MATRIX_SAVE_FLAG | Canvas.CLIP_SAVE_FLAG
                | Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.FULL_COLOR_LAYER_SAVE_FLAG
                | Canvas.CLIP_TO_LAYER_SAVE_FLAG;

        private Paint mPaint = new Paint();

        public SimpleView(Context context) {
            super(context);
        }

        @Override
        protected void onDraw(Canvas canvas) {
            canvas.drawColor(Color.WHITE);
            canvas.translate(10, 10);
            mPaint.setColor(Color.RED);
            canvas.drawCircle(75, 75, 75, mPaint);
            /**
             * 入栈一个Layer,当前现在有两个Layer,因为Canvas其实也可以看成是一个Layer
* 这里入栈了一个带有透明度的Layer *
*/ canvas.saveLayerAlpha(0, 0, 200, 200, 0x88, LAYER_FLAGS); mPaint.setColor(Color.BLUE); canvas.drawCircle(125, 125, 75, mPaint); canvas.restore(); } } } 

  然后我们调用restore()方法,那么画在透明层的蓝色圆圈就需要被重新绘制到当前的Canvas层上,因此可以看到,红色圆圈和蓝色圆圈叠加的状态,并且中间是有透明度的.如果比入栈一个新的图层,会出现明显的效果差异.大家可以将这句话注释掉运行一下看看效果.

 因为在头像裁切的时候我们需要使用多层画布结合XferMode实现复杂图形,因此先在这里简单的介绍一下,以免在后续看到这块代码不知道具体是要做什么用的.

ii.Xfermode

 Xfermode也是实现这个效果的一个核心,它是实现图形混合的一种模式,由Tomas Proter和 Tom Duff提出的概念,Xfermode只是一个基类,具有三个子类,分别是AvoidXfermode,PixelorXfermode,PorterDuffXfermode.不过前两个在api16以后就已经弃用了,因此前面这两个我也没打算说,主要还是说一下PorterDuffXfermode这种模式.

 PorterDuffXfermode一共有18种图形混排模式.那么就来介绍一下这18种模式,以及这18中模式的出现所导致的效果.

PorterDuff.Mode
 模式+说明  计算方式
PorterDuff.Mode
ADD(饱和相加)  Saturate(S + D)

  PorterDuff.Mode

CLEAR(清除图像)  [0,0] 
 
PorterDuff.Mode
DARKEN(变暗)  [Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)];
 
PorterDuff.Mode
DST(只绘制目标图像)  [Da, Dc]
 
PorterDuff.Mode
DST_ATOP(显示原图像非交集部分与目标图像交集部分) [Sa, Sa * Dc + Sc * (1 - Da)] 
 
PorterDuff.Mode
DST_IN(显示原图像与目标图像相交的目标图像部分) [Sa * Da, Sa * Dc]
 
PorterDuff.Mode
DST_OVER(原图像目标图像均显示,目标图像在上层)  [Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc]
 
PorterDuff.Mode
LIGHTEN(变亮)  [Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)] 
 
PorterDuff.Mode
MULTIPLY(显示原图像与目标图像交集部分叠加后的颜色) [Sa * Da, Sc * Dc] 
 
PorterDuff.Mode
OVERLAY(叠加)  未知 
 
PorterDuff.Mode
SCREEN(取原图像和目标图像的全部区域,交集部分为透明色)  [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc] 
 
PorterDuff.Mode
SRC(只显示原图像)  [Sa, Sc] 
 
PorterDuff.Mode
SRC_ATOP(显示原图像交集部分和目标图像的非交集部分)  [Da, Sc * Da + (1 - Sa) * Dc] 
 
PorterDuff.Mode
SRC_IN(显示原图像与目标图像交集部分的原图像)  [Sa * Da, Sc * Da] 
 
PorterDuff.Mode
SRC_OUT(显示原图像与目标图像非交集部分的目标图像)  [Sa * (1 - Da), Sc * (1 - Da)] 
 
PorterDuff.Mode
SRC_OVER(在目标图像的顶部绘制源图像)  [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] 
 
PorterDuff.Mode
XOR(去除两图层交集部分)  [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc] 
 
PorterDuff.Mode
DST_OUT(只在源图像和目标图像相交的地方绘制源图像)  [Da * (1-Sa), Dc * (1-Sa)] 

 

这就是18种PorterDuffMode的18种情况,相关的具体样式我就不在这里贴出来了.

public class XfermodeView extends View {

    /**
     * 18种图形混合模式
     */
    private static final PorterDuff.Mode PorterDuffMode[] = {PorterDuff.Mode.ADD, PorterDuff.Mode.CLEAR, PorterDuff.Mode.DARKEN,
            PorterDuff.Mode.DST, PorterDuff.Mode.DST_ATOP, PorterDuff.Mode.DST_IN, PorterDuff.Mode.DST_OUT, PorterDuff.Mode.DST_OVER,
            PorterDuff.Mode.LIGHTEN, PorterDuff.Mode.MULTIPLY, PorterDuff.Mode.OVERLAY, PorterDuff.Mode.SCREEN, PorterDuff.Mode.SRC,
            PorterDuff.Mode.SRC_ATOP, PorterDuff.Mode.SRC_IN, PorterDuff.Mode.SRC_OUT, PorterDuff.Mode.SRC_OVER, PorterDuff.Mode.XOR};

    private PorterDuffXfermode porterDuffXfermode;

    private int mode;

    private int defaultMode = 0;

    private static final int Layers = Canvas.MATRIX_SAVE_FLAG | Canvas.CLIP_SAVE_FLAG |
            Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.FULL_COLOR_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG;

    /**
     * 屏幕宽高
     */
    private int screenW;
    private int screenH;
    private Bitmap srcBitmap;
    private Bitmap dstBitmap;

    /**
     * 源图和目标图宽高
     */
    private int width = 120;
    private int height = 120;

    public XfermodeView(Context context) {
        super(context);

    }

    public XfermodeView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.XfermodeView);
        mode = typedArray.getInt(R.styleable.XfermodeView_ModeNum, defaultMode);
        screenW = ScreenUtil.getScreenW(context);
        screenH = ScreenUtil.getScreenH(context);
        porterDuffXfermode = new PorterDuffXfermode(PorterDuffMode[mode]);
        srcBitmap = makeSrc(width, height);
        dstBitmap = makeDst(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        Paint paint = new Paint();
        paint.setFilterBitmap(false);
        paint.setStyle(Paint.Style.FILL);
        /**
         * 绘制蓝色矩形+黄色圆形
         * */
        canvas.drawBitmap(srcBitmap, screenW / 8 - width / 4, screenH / 12 - height / 4, paint);
        canvas.drawBitmap(dstBitmap, screenW / 2, screenH / 12, paint);

        /**
         * 创建一个图层,在图层上演示图形混合后的效果
         * */
        canvas.saveLayer(0, 0, screenW, screenH, null, Layers);

        /**
         * 绘制在设置了XferMode后混合后的图形
         * */
        canvas.drawBitmap(dstBitmap, screenW / 4, screenH / 3, paint);
        paint.setXfermode(porterDuffXfermode);
        canvas.drawBitmap(srcBitmap, screenW / 4, screenH / 3, paint);
        paint.setXfermode(null);
        // 还原画布
        canvas.restore();
    }
}

  这段代码针对了18种不同模式显示的样式,原图像是一个蓝色的正方形,目标图像是一个黄色的圆形.然后我们另起了一个图层saveLayer(),将这18种模式出现的情况画在这个新的画布上.这里的代码并不是完全的,最后我会给出这个代码的地址,方便大家理解.

iii.自定义ClipView

 简单的介绍了一下Canvas和Xfermode,我们就可以使用自定义View,然后结合这二者实现头像的裁切效果.简单的说一下原理.

  上面这个图是实现头像裁切的原理,我们在Layer层放置一个ImageView,然后入栈一个图层,将ClipView画在Layer1上,然后使用Xfermode中的 DST_OUT 模式,这样取二者的相交部分,也就是ClipView这个圆圈与底部的ImageView的交集部分,并且显示ImageView部分.其他的地方就变成透明的了.

 

  就像上面这张图一样,显示的地方是二者的交集部分,裁剪框+底部的ImageView的共同部分,然后其他的地方都是透明的.这样我们就可以只获取二者交集部分的图像.那么具体如何实现这里,需要我们去自定义View实现.在上层的Layer需要自定义一个ClipView.这个View相对而言还是非常简单的.只需要在新的图层上用Paint画一个圆圈和圆边框就行了.然后设置Xfermode就可以轻松的实现.

   public ClipView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        /**
         * 去掉锯齿
         * */
        mPaint.setAntiAlias(true);
        borderPaint.setStyle(Paint.Style.STROKE);
        borderPaint.setColor(Color.WHITE);
        borderPaint.setStrokeWidth(clipBorderWidth);
        borderPaint.setAntiAlias(true);
        xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        width = this.getWidth();
        height = this.getHeight();

        /**
         * 另启一个图层,以后所有的绘制操作都在此图层上完成.
         * 不另外开启一个图层的话,Canvas在被掏空之后,背景色就不是透明的,而是黑的
         * */
        canvas.saveLayer(0, 0, width, height, null, LAYER_FLAGS);
        canvas.drawColor(Color.parseColor("#a8000000"));
        mPaint.setXfermode(xfermode);
        /**
         * 在画布上画透明的圆
         * */
        canvas.drawCircle(width / 2, height / 2, width * radiusWidthRatio, mPaint);
        /**
         * 圆边框
         * */
        canvas.drawCircle(width / 2, height / 2, width * radiusWidthRatio + clipBorderWidth, borderPaint);
        /**
         * 出栈,恢复到之前的图层,意味着新建的图层会被删除,新建图层上的内容会被绘制到canvas
         * */
        canvas.restore();
    }

  这里就不贴全部代码了,直接把核心代码粘贴出来就够了.代码和上面所说的思想基本是一致的.并且还有相关的注释,我就不多做解释了.这样我们也是仅仅实现了在新的Layer上画了这样一个圆圈.那么如何实现缩放和平移图片,然后获取到图片这才是比较重要的一个部分.

 既然要实现缩放和平移,那么必须要重写手势事件.这基本是习以为常的事情了.先贴代码,然后再说其中的道理.

@Override
    public boolean onTouch(View v, MotionEvent event) {

        ImageView view = (ImageView) v;
        switch (event.getAction() & MotionEvent.ACTION_MASK) {

            case MotionEvent.ACTION_POINTER_DOWN:
                oldDist = spacing(event);
                /**
                 * 如果间距大于10f,表示以后再MOVE的时候要进行缩放操作,而不是平移操作
                 * */
                if (oldDist > 10f) {
                    saveMatrix.set(matrix);
                    midPoint(midPoint, event);
                    mode = ZOOM;
                }
                break;
            case MotionEvent.ACTION_DOWN:
                /**
                 * 单指触发按下事件
                 * */
                saveMatrix.set(matrix);
                startPoint.set(event.getX(), event.getY());
                mode = DRAG;
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_POINTER_UP:
                mode = NONE;
                break;
            case MotionEvent.ACTION_MOVE:
                /**
                 * 拖动
                 * */
                if (mode == DRAG) {
                    matrix.set(saveMatrix);
                    matrix.postTranslate(event.getX() - startPoint.x, event.getY() - startPoint.y);
                } else if (mode == ZOOM) {
                    float newDist = spacing(event);
                    /**
                     * 如果两指拉开的间距大于10f那么就表示需要缩放
                     * */
                    if (newDist > 10f) {
                        matrix.set(saveMatrix);
                        float scale = newDist / oldDist;
                        matrix.postScale(scale, scale, midPoint.x, midPoint.y);
                    }
                }
                break;
        }
        /**
         * 每次操作结束后都需要设置matrix
         * */
        view.setImageMatrix(matrix);
        return true;
    }

  这里不难发现重写的事件要比以前多一些,因为这里不仅仅涉及到我们单指按下,单指按下屏幕一般就是平移图片,那么缩放的时候需要多指按下图片,通过拖动的方式实现缩放功能.

 这里定义了三个标记位,一个是NONE表示没有进行任何的操作,DRAG则表示拖动操作,ZOOM表示缩放操作。

 当我们单指按下的时候,首先需要记录下当前按下的坐标,然后改变标志位,因为单指按下一般后续就是DRAG操作,不可能发生ZOOM操作,因此在ACTION_DOWN之后需要改变标志位为DRAG如果我们后续进行了平移操作,也就是ACTION_MOVE 那么就会进行相关的处理,他会根据DRAG或ZOOM操作执行不同的逻辑,如果是DRAG,那么我们只需要根据移动后的坐标和起始按下的坐标对view进行平移操作就可以了,这个操作由matrix来决定.

 当我们两个手指同时按压到屏幕上的时候,这里做了一个简单的判断,就是两指之间的距离,如果距离小于10f,那么就还是表示要执行平移操作,否则执行缩放操作,那么当需要执行缩放操作的时候首先需要记录两指按下的中心点坐标,然后根据初始两指之间的距离和移动后两指之间的距离做除法运算,就可以计算出我们具体要缩放多少,缩放就是通过根据最开始的中心点以及matrix的配合实现缩放效果.最后基本就是获取图片随机生成一个uri返回就可以了.

 需要注意一点就是图片在放置到ImageView上的时候我们是需要对图片进行加工的,因为我们现在手机内部的图片已经不仅仅是720*1280那么简单了现在手机拍摄出来的图片像素一般是4000+*3000+的,这个取决于我们相机的像素,和屏幕的分辨率是没有什么关系的,因此在筛选完图片之后就需要对图片进行相关的处理.因此我为ClipView注册了一个视图树监听,也就是说当ClipView监听到整个视图树状态发生了相关的变化,那么就表示图片需要显示在ImageView上了,这时我们就需要对图片进行加工处理.每一个Layout都构成一个视图树,其实我感觉它和DOM树结构差不多,都是按层级划分的.还有注册完之后,触发的同时需要remove掉,否则会多次调用.

ViewTreeObserver observer = clipView.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        clipView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
        initSrc();
    }
});

  这样就通过Canvas+Xfermode+自定义ClipView实现了头像的裁切.裁切出来是个矩形的,只需要显示在圆形的ImageView上就可以了.这里圆形的头像大家也可以选择其他的类库,或者是自己自定义.我这里就不多说了,我是使用的第三方.最后贴一下源代码方便大家的理解.

 Canvas:http://pan.baidu.com/s/1mhSnkPM

 XferMode:http://pan.baidu.com/s/1dES108T

 圆形头像裁切:http://pan.baidu.com/s/1nvo5ORR

 

posted @ 2016-08-24 22:09  代码丶如风  阅读(1355)  评论(2编辑  收藏  举报