前言
最近项目中突然要将用到图片(项目使用Fresco)及视频(项目使用TextureView绘制纹理,SurfaceView不在本文讨论之列,绝大部分播放器为了视图可控,现在都会采用TextureView而不是SurfaceView。原因的话那又是另一片大海,自行脑补)的地方都进行圆角化,且需支持可控实验,即开关开启时圆角关闭时非圆角。由于工程运行已久,图片及视频的地方甚多,除了考虑技术方案外,还需考虑人工成本。
各种圆角化方案探索对比
方案一 : 直接采用Canvas.clipxxx 相关api,裁剪出一个圆角区域
emmmmmm...... 该方案简单暴力,通用性强。然后,全文终。
然后你就会发现,你的页面也终了。如果只是一个静态的单图视图,该方法问题不大,但如果是复杂页面,滚动的时候,测试就会跟你说,页面卡顿了,要优化。
原因就是 Canvas.clip的相关api损耗相对较大。
方案二 :直接Fresco自带功能
1: 直接使用 roundedCornerRadius属性,然后你就会惊奇地发现,静图很完美,动图无效了......
2: 经过一番查找,发现fresco已经给出动图的解决方案,加上roundWithOverlayColor属性,该属性支持传drawable类型,然后动图可以圆角化了
...泪大普奔,可以下班了。
但是 我们工程原来很多地方是类似头条这样,item有点击背景的.
而圆角只是在图片的各个角上,使用该属性实现的话,你会发现如下现象
-
问题一:外层背景颜色改变时,盖住的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以下的机子,圆角化并不是你想象中的样子
直接偷懒网上盗下效果图,如下
初步一看,虽然加上了圆角属性,但是图片边上是方的。将左下角和左上角放大仔细看下:
可以看到,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(这方面知识,自行脑补,赋上一张简图)
该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,有兴趣的同学,自行点击链接查看
总结
总的来说各种实现圆角的方案大概原理可概括为以下几种
1:直接裁剪视图型,简单暴力
2:利用各种图形重叠区域的api及模式,产生效果,但可能会有api版本问题
3:直接在原视图上盖层底色圆角。方案通用,但实现方式不一样,代码量及通用性可以相差不少