圆形ImageView系列(一)-----Xfermode+View

前言

看标题就知道,这是一个系列,因为实现圆形ImageView的方法有很多种,所以接下来准备将这几种方法都实现一遍,最后总结对比下各种方法。

虽然网上已经有很多现成的分析文章以及源代码,但是毕竟是别人总结的,还是没有自己实践来的真实。

纸上得来终觉浅,绝知此事要躬行哈。

下面就开始来说最简单常用的一种方法:自定义View+设置Xfermode的方式。先给大家看效果图:



提莫萌萌哒~

主要原理

关于Xfermode相关的知识,这里不多做介绍,只稍微提一下。会用ps的同学应该都用过选框工具,选框工具也有不同的模式,分别是:

  • 新选区
  • 添加到选区
  • 从选区减去
  • 与选区交叉

总之就是各种取交集、并集的过程。Xfermode的各种不同模式思想上跟这个有点类似,不过比ps的选区强大得多,大家看图就明白了:



可以看到Xfermode的取值有很多种,不同的取值可以达到不同的效果,类似于一些刮刮卡以及撕美女裙子等的游戏应该都可以利用设置Xfermode的不同值来实现。

16条Porter-Duff规则

对于上图中的不同取值,网上有人总结出来了下面16条规律:

  • 1.PorterDuff.Mode.CLEAR
    所绘制不会提交到画布上。

  • 2.PorterDuff.Mode.SRC
    显示上层绘制图片

  • 3.PorterDuff.Mode.DST
    显示下层绘制图片

  • 4.PorterDuff.Mode.SRC_OVER
    正常绘制显示,上下层绘制叠盖。

  • 5.PorterDuff.Mode.DST_OVER
    上下层都显示。下层居上显示。

  • 6.PorterDuff.Mode.SRC_IN
    取两层绘制交集。显示上层。

  • 7.PorterDuff.Mode.DST_IN
    取两层绘制交集。显示下层。

  • 8.PorterDuff.Mode.SRC_OUT
    取上层绘制非交集部分。

  • 9.PorterDuff.Mode.DST_OUT
    取下层绘制非交集部分。

  • 10.PorterDuff.Mode.SRC_ATOP
    取下层非交集部分与上层交集部分

  • 11.PorterDuff.Mode.DST_ATOP
    取上层非交集部分与下层交集部分

  • 12.PorterDuff.Mode.XOR

  • 13.PorterDuff.Mode.DARKEN

  • 14.PorterDuff.Mode.LIGHTEN

  • 15.PorterDuff.Mode.MULTIPLY

  • 16.PorterDuff.Mode.SCREEN

有的同学对于上面的src和dest可能分不清楚,究竟先画的是src还是dest呢?分析google给的android apidemo里面graphics/xfermode里面的onDraw方法可以看到

        @Override
        protected void onDraw(Canvas canvas) {
            canvas.drawColor(Color.WHITE);

            Paint labelP = new Paint(Paint.ANTI_ALIAS_FLAG);
            labelP.setTextAlign(Paint.Align.CENTER);

            Paint paint = new Paint();
            paint.setFilterBitmap(false);

            canvas.translate(15, 35);

            int x = 0;
            int y = 0;
            for (int i = 0; i < sModes.length; i++) {
                // draw the border
                paint.setStyle(Paint.Style.STROKE);
                paint.setShader(null);
                canvas.drawRect(x - 0.5f, y - 0.5f,
                                x + W + 0.5f, y + H + 0.5f, paint);

                // draw the checker-board pattern
                paint.setStyle(Paint.Style.FILL);
                paint.setShader(mBG);
                canvas.drawRect(x, y, x + W, y + H, paint);

                // draw the src/dst example into our offscreen bitmap
                int sc = canvas.saveLayer(x, y, x + W, y + H, null,
                                          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);
                canvas.translate(x, y);
                canvas.drawBitmap(mDstB, 0, 0, paint);
                paint.setXfermode(sModes[i]);
                canvas.drawBitmap(mSrcB, 0, 0, paint);
                paint.setXfermode(null);
                canvas.restoreToCount(sc);

                // draw the label
                canvas.drawText(sLabels[i],
                                x + W/2, y - labelP.getTextSize()/2, labelP);

                x += W + 10;

                // wrap around when we've drawn enough for one row
                if ((i % ROW_MAX) == ROW_MAX - 1) {
                    x = 0;
                    y += H + 30;
                }
            }
        }

这段代码完成的效果也就是上面那个xfermode效果对照表所展示的效果了,可以看到onDraw里面的第35~38行中,可以非常清楚的知道:

  • 先画mDstB

  • 然后设置xfermode

  • 最后画mSrc

也就是先画的图是作为dest画在canvas 的下层的,后画的图是作为src画在canvas的上层的。

对于我们这个圆形的ImageView的话,主要是使用srcIn的模式即可:在canvas下层画一个圆,然后将图片画在圆上,取两者的交集并且显示上层,就可以达到圆形图片的效果了。

方法如下:

  • 在canvas上画一个圆;
  • 调用paint.setXfermode设置为srcIn模式;
  • 将要显示的图作为Src画在上面的那个圆上;
  • 两次绘图之后叠加起来的效果就是圆形的图片了;

那么能不能先画图片然后画圆,将xfermode设置成destIn呢?试了一下发现不行,这是为什么呢?(用官方的demo调换绘制顺序并且改成destin是可以实现srcin的效果的)

主要原理已经介绍完了,当然除了设置Xfermode的值,还需要考虑其他一些小细节,比如图片和圆的相对位置、图片和view的相对大小等,纯粹就是计算了,也很简单。

具体实现

先贴代码吧。

先在attr.xml中声明两个自定义属性,以支持设置默认图片和初始图片,比如从网络获取一张图片时你可能需要一张默认图片显示,就可以设置默认图片了,代码如下

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <attr name="image" format="reference" />
    <attr name="defaultImage" format="reference" />

    <declare-styleable name="CircleImageView">
        <attr name="image" />
        <attr name="defaultImage" />
    </declare-styleable>

</resources>

然后在布局文件中声明xml命名空间,如下

 xmlns:circleImage="http://schemas.android.com/apk/res-auto"

接下来就可以使用自定义属性了

<com.passerby.androidadvanced.circleimage.xfermode.CircleImageView
        android:layout_width="200dp"
        android:layout_height="200dp"
        circleImage:defaultImage="@drawable/image"/>

CircleImageView源码如下:

package com.passerby.androidadvanced.circleimage.xfermode;

import com.passerby.circleimage.R;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;

/**
 * Created by mac on 16/2/1.
 */
public class CircleImageView extends View {

    private Paint mPaint;
    private Bitmap mSrcBitmap;
    private Bitmap mDefaultBitmap;

    public CircleImageView(Context context) {
        this(context, null);
    }

    public CircleImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyleAttr, 0);
        int count = a.getIndexCount();
        for (int i = 0; i < count; i++) {
            int resId;
            int index = a.getIndex(i);
            switch (index) {
                case R.styleable.CircleImageView_defaultImage:
                    resId = a.getResourceId(index, 0);
                    mDefaultBitmap = BitmapFactory.decodeResource(getResources(), resId);
                    break;
                case R.styleable.CircleImageView_image:
                    resId = a.getResourceId(index, 0);
                    mSrcBitmap = BitmapFactory.decodeResource(getResources(), resId);
                    break;
            }
        }
        a.recycle();

        if (null == mSrcBitmap) {

            mSrcBitmap = mDefaultBitmap;
        }

        mPaint = new Paint();
        mPaint.setAntiAlias(true);
    }

    @Override
    protected void onDraw(Canvas canvas) {

        int width = getMeasuredWidth();
        int height = getMeasuredHeight();
        //取宽、高中的较小值
        int min = width > height ? height : width;
        if (null != mSrcBitmap) {

            canvas.drawBitmap(createCircleBitmap(mSrcBitmap, min), 0, 0, mPaint);   
        }
    }

    /***
     * 将传入的bitmap转换成圆的正方形图片
     *
     * @param bitmap
     * @param min    目标bitmap的高
     * @return
     */
    private Bitmap createCircleBitmap(Bitmap bitmap, int min) {

        Paint paint = new Paint();
        paint.setAntiAlias(true);

        final int tWidth = getMeasuredWidth();
        final int tHeight = getMeasuredHeight();
        //创建一个新的bitmap,大小跟imageview一样
        Bitmap target = Bitmap.createBitmap(tWidth, tHeight, Bitmap.Config.ARGB_8888);        
        //下面所有的绘图操作均在这个target上面完成
        Canvas canvas = new Canvas(target);

        //1.画圆形
        //计算圆的圆心在imageview中的坐标
        int halfWidth = tWidth >> 1;
        int halfHeight = tHeight >> 1;
        //得到圆的半径
        int radius = min;
        //画圆
        canvas.drawCircle(halfWidth, halfHeight, radius >> 1, paint);

        //2.设置Xfermode
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

        //3.画图片
        //将图片画在imageview居中的位置
        Rect srcRect = new Rect(0, 0, mSrcBitmap.getWidth(), mSrcBitmap.getHeight());
        int offsetX = (tWidth - min) >> 1;
        int offsetY = (tHeight - min) >> 1;
        Rect dstRect = new Rect(offsetX, offsetY, offsetX + min, offsetY + min);
        canvas.drawBitmap(bitmap, srcRect, dstRect, paint);

        return target;
    }

    public void setImage(int resId) {
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId);
        setImage(bitmap);
    }

    public void setImage(Bitmap bitmap) {

        mSrcBitmap = bitmap;
        invalidate();
    }
}

代码第37~53行是获取属性,得到用户指定的默认图片和直接显示的图片。

55~58行,如果用户没有指定直接显示的图片,就使用默认图片。

67~70行,获取控件宽和高中的较小值min,因为要显示圆形图片,所以要以min作为那个圆的直径。

71~74行,画圆形图片。createCircleBitmap()方法是核心,主要注释都写好了,就不多提了。

对于上面代码实现的思路,是直接继承View然后将图片弄成圆形画出来,这种方法适合只需要圆形图片的需求,像用户头像之类的。

当然也可以继承ImageView然后在上面盖一个遮罩层,也可以实现圆形ImageView的效果,而且可以同时保留ImageView相关的很多属性。

第二种方法请看这里《 圆形ImageView系列(二)—–Xfermode+ImageView》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值