Android-解析自定义view之圆形头像的各类方案

我们可以看到很多app都采用了圆形头像,那么怎么绘制圆形头像才是性能最好?代码复用性最强?也最方便呢?本博主做了一些探究。为了更全面解析,没做得很漂亮,可是这样更助于你理解。本博主不喜欢华而不实的东西。


文章结构:1.利用shape来制作圆形头像(一种死方案,要求是美工愿意配合你) 2.结合一个会导致oom的实现圆形头像方案进行性能分析 3.最优的圆形头像方案


先给效果图大家看看,上面的是用shape实现,下面的是用刚刚所说的最优方案实现
这里写图片描述


一、利用shape来制作圆形头像(要求是美工愿意配合你)

为什么要求美工配合你呢??因为这个方案是在ImageView直接调用资源文件的,也就是直接用了ImageView的LayoutParams的match_parent模式。不能按照那个圆的大小来适配。

给出代码讲解:drawable文件的shape标签

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <!-- Corner的属性是设置圆角的半径的-->

    <solid android:color="#FFFFFF" />
    <stroke
        android:width="2dp"
        android:color="#777777"></stroke>
    <size
        android:width="120dp"
        android:height="120dp" />
</shape>

在xml文件中的调用:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.demo.fuzhucheng.someShapesImageview.ImageViewActivity">
<!--第一种方案 -->
    <ImageView
        android:id="@+id/shapecircle"
        android:layout_width="150dp"
        android:layout_height="150dp"
        android:background="@drawable/activity_circle_circleimageview"
        android:src="@drawable/activity_imageview_photo" />

    <!--最优方案,也就是本文的第三种方案 -->
<com.demo.fuzhucheng.someShapesImageview.CircleImageview
        android:id="@+id/mycircle"
        android:layout_width="180dp"
        android:layout_height="180dp"
        android:background="@color/white"
        android:src="@drawable/timg"
        app:backgroundHeadColor="@color/yellow"
        app:circleBorderHeadWidth="5dp"
        app:ringHeadColor="@color/colorAccent" />
</LinearLayout>

二、对一种容易导致OOM的方案进行分析:

给出代码分析:下面这个是别人的代码,由于点评就不给链接的。

public class CircleImageView extends ImageView{
    public CircleImageView(Context context, AttributeSet    attrs) {
        super(context, attrs);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        //获得图片的宽度
        int width=getWidth();
        //获得图片的高度
        int height=getHeight();
        //短的二分之一作为半径
        int radius=height>width?width/2:height/2;

        //重新定义的一个画布,这一步很关键
        Paint mPaint = new Paint();
        //抗锯齿
        mPaint.setAntiAlias(true);
        Bitmap bitmap = Bitmap.createBitmap(width,height,
                Bitmap.Config.ARGB_8888);
        Canvas bitmapCanvas = new Canvas(bitmap);
        super.onDraw(bitmapCanvas);

        //圆形的框
        Bitmap cB = Bitmap.createBitmap(width, height,
                Bitmap.Config.ARGB_8888);
        Canvas cCanv = new Canvas(cB);
        //在控件中间画一个
        cCanv.drawCircle(width/ 2, height/ 2, radius,
                mPaint);

        canvas.drawBitmap(bitmap, 0.0f, 0.0f, mPaint);
        //dst是后画的图形
        mPaint.setXfermode(new PorterDuffXfermode(
                PorterDuff.Mode.DST_IN));
        //一定要用之前的画布,不然会出现边角是黑色
        bitmapCanvas.drawBitmap(cB, 0.0f, 0.0f, mPaint);

        //给图形加边框
        Paint paint =new Paint();
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(5);
        paint.setColor(Color.BLACK);
        canvas.drawCircle(width/ 2, height/ 2, radius,
                paint);

    }
}

分析:在这个方案中,1.因为我们在xml设置的imageview的画布就是占据了一个矩形我们需要重新定义一个画布,而怎么重新定义画布呢,就是重写onDraw然后在他继承父类方法属性前重新定义画布,也就是在super方法前面啦!!可是,这个方法涉及到渲染多层,很容易oom。

2.首先我们要知道画布是怎样一个原理,是基于渲染层的(其实是一个bitmap层存给画布canvas),而这里就先重定义画布一层来作为覆盖掉原来的canvas一层,然后再new了一个canvas进而在里面画一个圆来限定圆形头像来覆盖掉刚刚新建的下面一层canvas。然后利用在super前新建的画布来drawBitmap来画图,进而显示出一个圆形头像(同样经过裁剪)。所以这个涉及渲染多层的方案不可取,而且这段代码设计太过于混乱。


三、最优的圆形头像方案

在这里说明一下整体的思路:1.还是继承ImageView,覆写ImageView,在得到图片后进行处理,没设置图片的时候不处理。 2.使用BitmapShader画圆形的,只要把bitmap传进去,然后把Matrix也传进去作为图片缩放的工具(同样是大图片经过裁剪的方案,无法避免),然后就是set给画笔


下面给出方案的详解,结合代码有详细解释

package com.demo.fuzhucheng.someShapesImageview;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Shader;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.ImageView;

import com.demo.fuzhucheng.R;

/**
 * Created by ${符柱成} on 2016/8/21.
 */
public class CircleImageview extends ImageView {
    private Paint mPaintCircle;      //画圆形图像的笔
    private Paint mPaintBorder;          //画圆形边界的笔
    private Paint mPaintBackgroud;      //画背景颜色的笔
    private BitmapShader mBitmapShader;      //图像着色器,可以用来画圆
    private Matrix mMatrix;          //图片变换处理器-用来缩放图片以适应view控件的大小
    private int mWidth;        //获得控件宽度
    private int mHeight;             //获得控件高度
    private int mRadius;             //中心园的半径
    private int mCircleBorderWidth;        //边界宽度
    private int mCirlcleBorderColor;             //边界边框颜色
    private int mCircleBackgroudColor;      //圆形头像背景色

    public CircleImageview(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleHead);//将获取的属性转化为我们最先设好的属性
        int n = typedArray.getIndexCount();
        for (int i = 0; i < n; i++) {
            int attr = typedArray.getIndex(i);
            switch (attr) {
                case R.styleable.CircleHead_circleBorderHeadWidth:
                    mCircleBorderWidth = (int) typedArray.getDimension(attr, 0);
                    break;
                case R.styleable.CircleHead_ringHeadColor:
                    mCirlcleBorderColor = typedArray.getColor(attr, Color.GREEN);
                    break;
                case R.styleable.CircleHead_backgroundHeadColor:
                    mCircleBackgroudColor = typedArray.getColor(attr, Color.YELLOW);
                    break;
            }
        }
        init();
    }

    private void init() {
        //初始化图片变换处理器
        mMatrix = new Matrix();

        mPaintCircle = new Paint();
        mPaintCircle.setAntiAlias(true);//抗锯齿,没有消除锯齿的话,图片变换会很难看的,因为有些像素点会失真
        mPaintCircle.setStrokeWidth(12);                     //设置圆边界宽度
        //附加效果设置阴影
        this.setLayerType(LAYER_TYPE_SOFTWARE, mPaintCircle);
        mPaintCircle.setShadowLayer(13.0f, 5.0f, 5.0f, Color.GRAY);


        //给图形加边框
        mPaintBorder = new Paint();
        mPaintBorder.setAntiAlias(true);
        mPaintBorder.setStyle(Paint.Style.STROKE);
        mPaintBorder.setStrokeWidth(mCircleBorderWidth);
        mPaintBorder.setColor(mCirlcleBorderColor);

        //画背景颜色的笔
        mPaintBackgroud = new Paint();
        mPaintBackgroud.setColor(mCircleBackgroudColor);
        mPaintBackgroud.setAntiAlias(true);
        mPaintBackgroud.setStyle(Paint.Style.FILL);
    }

    //使用BitmapShader画圆图形
    private void setBitmapShader() {
        //将图片转换成Bitmap
        Drawable drawable = getDrawable();
        BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
        Bitmap bitmap = bitmapDrawable.getBitmap();
        //将bitmap放进图像着色器,后面两个模式是x,y轴的缩放模式,CLAMP表示拉伸
        mBitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
        //初始化图片与view之间伸缩比例,因为比例一般非整数,所以用float,免得精度丢失
        float scale = 1.0f;
//        float scaleX = 1.0f;
        //将图片的宽度高度的最小者作为图片的边长,用来和view来计算伸缩比例
//        int bitmapSize = Math.min(bitmap.getWidth(), bitmap.getHeight());
        int bitmapSize = Math.min(bitmap.getHeight(), bitmap.getWidth());


        //        int bitmapSizeX = bitmap.getWidth();
        //      scaleX = mWidth * 1.0f / bitmapSizeX;
        /**注意这里,我使用的是图片最长的(就是宽度)来伸缩,那么用这个的话,
         * 我们就会发现,较短的那边(就是高度)在经过Matrix的拉伸后会发现失真,强行地被拉长,
         * 一、因为图片为了适应最长的那边可以完全在view上展示,把长的给压缩了,而短的比长的那边短,所以要强行拉伸,那么就会导致短的这边被拉伸时候失真
         *二、因为图像的变换是针对每一个像素点的,所以有些变换可能发生像素点的丢失,
         * 这里需要使用Paint.setAnitiAlias(boolean)设置来消除锯齿,这样图片变换后的效果会好很多。
        
         */

        //计算缩放比例,view的大小和图片的大小比例
        scale = mWidth * 1.0f / bitmapSize;
        //利用这个图像变换处理器,设置伸缩比例,长宽以相同比例伸缩
        mMatrix.setScale(scale, scale);
        //给那个图像着色器设置变换矩阵,绘制时就根据view的size,设置图片的size
        //使图片的较小的一边缩放到view的大小一致,这样就可以避免图片过小导致CLAMP拉伸模式或过大导致显示不全
        mBitmapShader.setLocalMatrix(mMatrix);
        //为画笔套上一个Shader的笔套
        mPaintCircle.setShader(mBitmapShader);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        /**
         * 在这里设置高度宽度,以设置的较小值为准,防止不成圆
         */
        mWidth = getWidth();
        mHeight = getHeight();
        int mCircleSize = Math.min(mHeight, mWidth);
        //圆的半径短的二分之一作为半径
        mRadius = mCircleSize / 2 - mCircleBorderWidth;
    }

    /**
     * 我们可以知道,如果我们直接用imageview然后引用shape弄成圆形的话,意味着我们在这个imageview的逻辑只能写在fragment等等里面了,而很难去进行逻辑的分层.。而且!!只能用矢量图并且美工要配合你
     * 因此我们重写imageview就是为了更好地封装好点击imageview的逻辑
     * 一、因为我们在xml设置的imageview的画布就是占据了一个矩形我们需要重新定义一个画布
     * 而怎么重新定义画布呢,就是重写onDraw然后在他继承父类方法属性前重新定义画布,也就是在super方法前面啦!!可是,这个方法涉及到渲染多层,很容易oom
     * 二、然而我们将用另一种方法,使用BitmapShader画圆形的,只要把bitmap传进去,然后把Matrix也传进去作为图片缩放的工具
     */

    /*
       * Canvas理解成系统提供给我们的一块内存区域(但实际上它只是一套画图的API,真正的内存是下面的Bitmap),
        *而且它还提供了一整套对这个内存区域进行操作的方法,所有的这些操作都是画图API。
        *也就是说在这种方式下我们已经能一笔一划或者使用Graphic来画我们所需要的东西了,要画什么要显示什么都由我们自己控制。
     */
    @Override
    protected void onDraw(Canvas canvas) {
        //这里注释掉onDraw是为了不绘制原来的画布,如果使用的话就意味着又是渲染一层,就会像第二个方案那样容易OOM
        //        super.onDraw(canvas);
        if (getDrawable() != null) {
            setBitmapShader();
            canvas.drawRect(0, 0, mWidth, mHeight, mPaintBackgroud);//直接构造,画背景的,为什么画背景?因为画布是方的,市面上所有圆形头像都是没有直接处理边角的,而是用Framelayout来进去覆盖,所以这里定义个背景色告诉大家,当然也封装好给大家使用
            canvas.drawCircle(mWidth / 2, mHeight / 2, mRadius, mPaintCircle);
            //画边框
            canvas.drawCircle(mWidth / 2, mHeight / 2, mRadius + mCircleBorderWidth / 2, mPaintBorder);
        } else {
        //如果在xml中这个继承ImageView的类没有被set图片就用默认的ImageView方案咯
            super.onDraw(canvas);
        }

    }
}

还有在自定义属性的代码,因为本博文不只是解析嘛,顺便封装给大家使用。可以看到有三个属性,边框宽度,背景颜色以及圆环颜色

    <declare-styleable name="CircleHead">
        <attr name="circleBorderHeadWidth" format="dimension" />
        <attr name="ringHeadColor" format="color" />
        <attr name="backgroundHeadColor" format="color" />
    </declare-styleable>

至于调用的方式就在第一种方案中已经给出就不重复贴代码了。


另外大家有没有留意到我写的那两个方案以及别人的方案(本文第二个方案)都是有边角没处理的?因为画布是方的,市面上所有圆形头像都是没有直接处理边角的,而是用Framelayout来进去覆盖,所以这里定义个背景色告诉大家,当然也封装好给大家使用。福利好吧?


好了,自定义view之圆形头像的各类方案已经解说完毕。欢迎在下面指出错误,共同学习!

转载请注明:【JackFrost的博客】

更多精彩的内容,可以访问JackFrost的博客

本博主最近会一直更新各类自定义view的方案,欢迎关注。

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

推荐阅读更多精彩内容