Android 视图圆角化处理方案

前言

最近项目中突然要将用到图片(项目使用Fresco)及视频(项目使用TextureView绘制纹理,SurfaceView不在本文讨论之列,绝大部分播放器为了视图可控,现在都会采用TextureView而不是SurfaceView。原因的话那又是另一片大海,自行脑补)的地方都进行圆角化,且需支持可控实验,即开关开启时圆角关闭时非圆角。由于工程运行已久,图片及视频的地方甚多,除了考虑技术方案外,还需考虑人工成本。

各种圆角化方案探索对比

方案一 : 直接采用Canvas.clipxxx 相关api,裁剪出一个圆角区域

emmmmmm...... 该方案简单暴力,通用性强。然后,全文终。


黑脸

然后你就会发现,你的页面也终了。如果只是一个静态的单图视图,该方法问题不大,但如果是复杂页面,滚动的时候,测试就会跟你说,页面卡顿了,要优化。
原因就是 Canvas.clip的相关api损耗相对较大。

方案二 :直接Fresco自带功能

1: 直接使用 roundedCornerRadius属性,然后你就会惊奇地发现,静图很完美,动图无效了......

2: 经过一番查找,发现fresco已经给出动图的解决方案,加上roundWithOverlayColor属性,该属性支持传drawable类型,然后动图可以圆角化了
...泪大普奔,可以下班了。

但是 我们工程原来很多地方是类似头条这样,item有点击背景的.


item点击背景改变.gif

而圆角只是在图片的各个角上,使用该属性实现的话,你会发现如下现象


overlayColor透出4个角不同颜色.gif
  • 问题一:外层背景颜色改变时,盖住的4个角的颜色,并无随外层点击颜色变化,4个角还是上次的默认颜色透出
    这个现象,如果圆角不是很大,且大item的不同状态间颜色差异不大时,不是很明显。原因roundWithOverlayColor属性采用的是一个普通的静态drawable,当外部背景按下时OverlayDrawable,并无刷新。

    那我们就在外部背景按下时,重新设置roundWithOverlayColor色值,然后重新调用加载图片?
    原理可行,但通过fresco二次加载的方式,性能还是有点浪费。如果项目中不用处理视频,或允许视频与图片2套的化,图片圆角化可以采用该方案。

    该方案应该有点可以优化,就是不要调用二次加载方式,而是按下去时
    获取OverlayDrawable(没再认真去看源码,是否叫这名字,暂时这样叫)然后根据颜色刷新该层drawable就好。Fresco的原理就是一层层的drawable,然后控制器根据当前状态,来显示对应层的drawable,猜测roundWithOverlayColor应该是有单独对应一层drawable的。由于我没采用该方案,具体细节不再细究。

如果采用该方案,那接下来就又要开启视频的圆角方案之旅了

方案三 :最终大招 CardView

经过一番思考后,我们终于想到了 系统提供的CardView,然后我们就开始了 全工程改造。
把原来全工程各个视频控件和图片控件的外层,都加上一层CardView,经过多个日夜不停地加班奋战,几天过去了,你就会发现,一切运行完美,视频控件也完美支持圆角化了

  • 问题二:每个视频控件和图片控件外层都加上个cardview,做为父layout的话,成本实在太高了。而且个别地方,原来如果是通过childview.getLayoutParams操作原子控件LayoutParams的话,那代码和布局同时改起来,简直是...

  • 问题三:套一层的话相当于多一层布局,布局层级更深一层,layout时间加长,性能上面你懂的。在开关关(无需圆角)的情况下,该cardview纯属浪费

  • 问题四:android 5.0 以下的机子你会发现神奇的现象,就是api 21以下的机子,圆角化并不是你想象中的样子
    直接偷懒网上盗下效果图,如下

    API21及以上.jpg

    api21以下.jpg

    初步一看,虽然加上了圆角属性,但是图片边上是方的。将左下角和左上角放大仔细看下:
    左下角细节图.jpg

    左上角.jpg

    可以看到,CardView本身是圆角效果了,但是里边的内容却还是方的,并且出现了多余的白边。
    看来是时候撸一把cardview源码(基于support 26.0.1其余版本大同小异,这里只分析粘贴最主要代码)了

public class CardView extends FrameLayout {

  ...
    static {
        if (Build.VERSION.SDK_INT >= 21) {
            IMPL = new CardViewApi21Impl();
        } else if (Build.VERSION.SDK_INT >= 17) {
            IMPL = new CardViewApi17Impl();
        } else {
            IMPL = new CardViewBaseImpl();
        }
        IMPL.initStatic();
    }

   
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (!(IMPL instanceof CardViewApi21Impl)) {
            final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            switch (widthMode) {
                case MeasureSpec.EXACTLY:
                case MeasureSpec.AT_MOST:
                    final int minWidth = (int) Math.ceil(IMPL.getMinWidth(mCardViewDelegate));
                    widthMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(minWidth,
                            MeasureSpec.getSize(widthMeasureSpec)), widthMode);
                    break;
            }

            final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            switch (heightMode) {
                case MeasureSpec.EXACTLY:
                case MeasureSpec.AT_MOST:
                    final int minHeight = (int) Math.ceil(IMPL.getMinHeight(mCardViewDelegate));
                    heightMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(minHeight,
                            MeasureSpec.getSize(heightMeasureSpec)), heightMode);
                    break;
            }
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        } else {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }

    private void initialize(Context context, AttributeSet attrs, int defStyleAttr) {
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CardView, defStyleAttr,
                R.style.CardView);
        ColorStateList backgroundColor;
        if (a.hasValue(R.styleable.CardView_cardBackgroundColor)) {
            backgroundColor = a.getColorStateList(R.styleable.CardView_cardBackgroundColor);
        } else {
            // There isn't one set, so we'll compute one based on the theme
            final TypedArray aa = getContext().obtainStyledAttributes(COLOR_BACKGROUND_ATTR);
            final int themeColorBackground = aa.getColor(0, 0);
            aa.recycle();

          ...

        IMPL.initialize(mCardViewDelegate, context, backgroundColor, radius,
                elevation, maxElevation);
    }

   ...

    };

最主要就是
1:初始化获取一些xml属性,变成本地变量方便后续使用
2: onMeasure方法,在api21以下做了特殊处理(一直很想吐槽这个处理方式),具体处理下文分析
3: 根据sdk版本生成不同的实现类 ,cardview只是做为一个空壳,在各种方法被系统调用的时候,调用对应实现类的对应方法。这就是为啥不同api版本,效果不一样的地方了。

既然问题出现在21以下,我们就先看下CardViewApi17Impl的实现。

其实17~20(CardViewApi17Impl)及17以下(CardViewBaseImpl)的差别很小,仅是在如何绘制圆角上方法(drawRoundRect)不同而已。原因如下,不再详细分析

 // Draws a round rect using 7 draw operations. This is faster than using
        // canvas.drawRoundRect before JBMR1 because API 11-16 used alpha mask textures to draw
        // shapes.

所以我们直接看CardViewBaseImpl,重点在以下3个方法

    @Override
    public void initialize(CardViewDelegate cardView, Context context,
            ColorStateList backgroundColor, float radius, float elevation, float maxElevation) {
        RoundRectDrawableWithShadow background = createBackground(context, backgroundColor, radius,
                elevation, maxElevation);
        background.setAddPaddingForCorners(cardView.getPreventCornerOverlap());
        cardView.setCardBackground(background);
        updatePadding(cardView);
    }

    private RoundRectDrawableWithShadow createBackground(Context context,
                    ColorStateList backgroundColor, float radius, float elevation,
                    float maxElevation) {
        return new RoundRectDrawableWithShadow(context.getResources(), backgroundColor, radius,
                elevation, maxElevation);
    }

    @Override
    public void updatePadding(CardViewDelegate cardView) {
        Rect shadowPadding = new Rect();
        getShadowBackground(cardView).getMaxShadowAndCornerPadding(shadowPadding);
        cardView.setMinWidthHeightInternal((int) Math.ceil(getMinWidth(cardView)),
                (int) Math.ceil(getMinHeight(cardView)));
        cardView.setShadowPadding(shadowPadding.left, shadowPadding.top,
                shadowPadding.right, shadowPadding.bottom);
    }

其实就是又把大部分工作交给了RoundRectDrawableWithShadow处理,然后将创建该drawable对象设为背景。然后本类中只处理一些外层间距问题,主要是外层阴影在21以下的实现方式(又是一个坑),最后我们跟踪RoundRectDrawableWithShadow,重点在

   @Override
    public void draw(Canvas canvas) {
        if (mDirty) {
            buildComponents(getBounds());
            mDirty = false;
        }
        canvas.translate(0, mRawShadowSize / 2);
        drawShadow(canvas);
        canvas.translate(0, -mRawShadowSize / 2);
        sRoundRectHelper.drawRoundRect(canvas, mCardBounds, mCornerRadius, mPaint);
    }

通过buildComponents及drawShadow方法,就会发现,其实在旧版本上,是采用设padding,讲cardview包裹的原布局缩小2层(1层用于显示阴影、1层用于绘制上图看到的原视频外的白色圆角部分),个人感觉这种方式灰常坑,首先强制将原来控件尺寸(ui要找麻烦了)改了不说,空出来绘制得出的阴影也是很呵呵,与21以上的效果完全不是一个级别;圆角效果也是相当于外层多了个圆角,而不是原视图上做的改动。这就明白了,为啥会出现上图的样子了。

那为啥5.0以上的效果会没有问题呢? 接下来就是重点CardViewApi21Impl,其实大概流程与CardViewApi17Impl 干的事差不多,区别仅在于drawable不一样,他是使用RoundRectDrawable做背景。继续跟踪RoundRectDrawable发现其实做的事与RoundRectDrawableWithShadow差不多。重点在于

  /**
     * Ensures the tint filter is consistent with the current tint color and
     * mode.
     */
    private PorterDuffColorFilter createTintFilter(ColorStateList tint, PorterDuff.Mode tintMode) {
        if (tint == null || tintMode == null) {
            return null;
        }
        final int color = tint.getColorForState(getState(), Color.TRANSPARENT);
        return new PorterDuffColorFilter(color, tintMode);
    }

该PorterDuff.Mode为PorterDuff.Mode.SRC_IN(这方面知识,自行脑补,赋上一张简图)


PorterDuff.jpg

该drawble 默认在4个角用透明像素绘制了4个圆角,然后结合PorterDuff.Mode.SRC_IN模式。
最后最关键的是要配合上view的setClipToOutline方法,就可以实现视图圆角了,但是这些api都是21及以后才有的,所以你懂的。

虽然cardview的方式,不适合我们。但是,api 21以后的这种方式给我们提供了一种思路,只需要设个RoundRectDrawable(support包中该类不对外开发,我们可以自行复制实现)当背景,然后打开view的setClipToOutline方法。2行代码即可搞定,瞬间解决了以上所有遇到问题

方案四 :痛苦的兼容及及全通用方案处理

如果项目可以不用兼容5.0以下机子,该部分可以不看了

原理:自己造轮子在view的最上层绘制一层与背景一样颜色的圆角,挡住下面的视图
原理很简单,但实现起来有有几个点要注意:
1:必须能先知道外层布局各种状态的底色,且外层背景颜色如果并非单色,那就凉凉了

  • 像这种外层有相应点击事件的情况,外层view还需要通知里层view 刷新对应圆角颜色;而且里层view无论有没单独的点击事件。盖上去的这层都不能根据本身view的状态变色,否则也会出现问题一的情况
  • 工程中有换肤功能,还必须兼容各种换肤情况

2:技术选型上,绘制覆盖层的圆角是否直接在view上绘制。直接在view上绘制可行,但通用性相对较低,相当于各种需要圆角的view都要去自定义一个原来的子类,在原子类上绘制一层。并且外层view状态的传递给里层view的方式代码写起来也会相对抠脚。建议采用drawable的方式,在各个view上层绘制一次该drawable即可。然后内外两层view用户操作状态的传递及如何刷新,实现方案不一,代码及工程量也不一

在这里就不再详细对比各种细节,直接上个人思考良久,综合各方面考量,最终觉得比较合理的方式,转换成demo,有兴趣的同学,自行点击链接查看

ConnerDemo

总结

总的来说各种实现圆角的方案大概原理可概括为以下几种
1:直接裁剪视图型,简单暴力
2:利用各种图形重叠区域的api及模式,产生效果,但可能会有api版本问题
3:直接在原视图上盖层底色圆角。方案通用,但实现方式不一样,代码量及通用性可以相差不少

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