Android之圆形头像裁切

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

 

学习内容:canvas

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

 

  头像裁切现现在在不少应用中都会获得使用,通常都是在我的资料页面设置头像,而后选择图片,或者是直接开启相机拍摄一张图片,经过裁切和缩放的手段最后显示在ImageView上就能够了.不过不管怎样裁切其实最后裁切出来仍然是一个方形的图片,所以咱们须要自定义ImageView,将ImageView定义成咱们想要的形状,而后将裁切到的图片显示到ImageView上就能够了.这里的ImageView我是使用的第三方框架,由于本身尚未打算自定义ImageView这块.所以就直接用了别人的东西.app

 

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

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

 

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

 这里咱们在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实现复杂图形,所以先在这里简单的介绍一下,以避免在后续看到这块代码不知道具体是要作什么用的.this

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

相关文章
相关标签/搜索