44

Android 视图圆角化处理方案 - 简书

 4 years ago
source link: https://www.jianshu.com/p/b24683e29aff?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
0.0972019.08.09 19:23:55字数 2,668阅读 1,674

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

各种圆角化方案探索对比

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

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

webp

然后你就会发现,你的页面也终了。如果只是一个静态的单图视图,该方法问题不大,但如果是复杂页面,滚动的时候,测试就会跟你说,页面卡顿了,要优化。
原因就是 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:直接在原视图上盖层底色圆角。方案通用,但实现方式不一样,代码量及通用性可以相差不少


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK