Android 视图圆角化处理方案

前言

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

各种圆角化方案探索对比

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

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


Android 视图圆角化处理方案_第1张图片
黑脸

然后你就会发现,你的页面也终了。如果只是一个静态的单图视图,该方法问题不大,但如果是复杂页面,滚动的时候,测试就会跟你说,页面卡顿了,要优化。
原因就是 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以下的机子,圆角化并不是你想象中的样子
    直接偷懒网上盗下效果图,如下

    Android 视图圆角化处理方案_第2张图片
    API21及以上.jpg

    Android 视图圆角化处理方案_第3张图片
    api21以下.jpg

    初步一看,虽然加上了圆角属性,但是图片边上是方的。将左下角和左上角放大仔细看下:
    Android 视图圆角化处理方案_第4张图片
    左下角细节图.jpg

    Android 视图圆角化处理方案_第5张图片
    左上角.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(这方面知识,自行脑补,赋上一张简图)


Android 视图圆角化处理方案_第6张图片
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:直接在原视图上盖层底色圆角。方案通用,但实现方式不一样,代码量及通用性可以相差不少

你可能感兴趣的:(Android 视图圆角化处理方案)