前言
最近項目中突然要將用到圖片(項目使用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以下的機子,圓角化并不是你想象中的樣子
直接偷懶網上盜下效果圖,如下
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(這方面知識,自行腦補,賦上一張簡圖)
該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:直接在原視圖上蓋層底色圓角。方案通用,但實現方式不一樣,代碼量及通用性可以相差不少