Android進階:最簡單的方式實現自定義陰影效果
網話說UI設計有三寶 :透明,陰影,加圓角。很多UI在做設計的時候都喜歡做卡片形式,然後新增陰影。卡片UI確實挺好看,但是對Android開發者來說,顯示陰影卻並不那麼手到擒來,因為Android對陰影沒有做出很好的支援。
CardView
谷歌也許早就注意到了UI的三寶之一陰影,於是開發了一個繼承FrameLayout的CardView公開發這使用,這個控制元件雖然在v7包裡,但是需要單獨新增依賴才可以使用,就好像不是親生的似的!
CardView本質上繼承FrameLayout,需要新增依賴才可以使用:
compile 'com.android.support:cardview-v7:25.3.1'
當你知道它繼承FrameLayout的時候你就知道怎麼使用了,但是這個CardView有很多侷限性,比如不能修改陰影的顏色,不能修改陰影的深淺。這就很詭異了,根本無法滿足UI設計潮流的內心。
那為了產品蒸蒸日上,早日走上人生巔峰,實現財富自由,應該如何讓你的APP支援修改陰影的顏色呢?
有個很暴力的辦法,就是吧CardView的程式碼自己摳出來,然後自己定製,網上已經有很多人這樣做了。
比如這篇CSDN博主就這樣做了: https://blog.csdn.net/wangjie_de/article/details/82993017
思路:修改谷歌原生的CardView程式碼:原生的CardView的陰影邏輯分為高版本21以上的和低版本21一下的兩種方案處理,其中低版本使用了漸變色來初六陰影漸變的效果,而高版本使用了 Elevation來設定陰影,但是 Elevation又沒提供修改顏色的方法,所以作者就把高版本的實現方案拿掉了,統一採用低版本的處理方法,就可以修改顏色了。具體過程請看其部落格。
但是現在我自己探索了一個新的較為簡單的新增陰影的實現方案,僅供參考
ShadowCardView
思路:首先要明確陰影的實現思路是什麼,其實就是顏色導致的視覺錯覺。說白了就是在你的Card周圍畫一個漸變的體現立體感的顏色。
基於上述思路,我們在一個在一個view上畫一個矩形的圖形,讓他周圍有漸變色的陰影即可。於是我們想起幾個API:
- 類:Paint 用於在Android上畫圖的類,相當於畫筆
- 類:Canvas 相當於畫布,Android上的view的繪製都與他相關
- 方法:paint.setShadowLayer可以給繪製的圖形增加陰影,還可以設定陰影的顏色
如圖,紅色部分是我們繪製的圖形,邊框以內,紅色之外的是陰影的顯示部分。

我們知道谷歌開發的CardView的控制元件繼承了FrameLayout,方便我們自由擴充套件。那麼我們也需要繼承FrameLayout。
ShadowCardView繼承FrameLayout之後,可以重寫其一個方法:
@Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); }
這個方法是ViewGroup在繪製子View的時候呼叫的,那麼我們可以在這個時候進行陰影的繪製。
首先,這個方法已經為我們提供了這個View的畫布:Canvas,我們可以直接在上面進行陰影的繪製,程式碼如下:
Paint shadowPaint = new Paint(); shadowPaint.setColor(Color.RED); shadowPaint.setStyle(Paint.Style.FILL); shadowPaint.setAntiAlias(true); float left = 45; float top = 45; float right = getWidth() - 45; float bottom = getHeight() - 45; shadowPaint.setShadowLayer(45, 0, 0, getContext().getResources().getColor(R.color.color_000000)); RectF rectF = new RectF(left, top, right, bottom); canvas.drawRoundRect(rectF, 0, 0, shadowPaint); canvas.save();
- 建立畫筆,設定畫筆的顏色,風格
- 獲取繪畫的範圍:ShadowCard的範圍減去需要的陰影的範圍,假如陰影的寬度為45px,則在ShadowCard內部的45px內進行繪製
- 給畫筆設定陰影的顏色,陰影的模糊度,模糊度值越大越模糊,且不能為0
- 建立RectF,最後開始繪畫。
這樣陰影就可以成功繪製了,這個方法程式碼量很少,很簡單,也很實用。

為了更好的封裝,我們可以為上面需要的引數進行定製,比如陰影的顏色,陰影的寬度,陰影的上下偏移,陰影的模糊度。
程式碼如下:
public class ShadowViewCard extends FrameLayout { private static final int DEFAULT_VALUE_SHADOW_COLOR = R.color.shadow_default_color; private static final int DEFAULT_VALUE_SHADOW_CARD_COLOR = R.color.shadow_card_default_color; private static final int DEFAULT_VALUE_SHADOW_ROUND = 0; private static final int DEFAULT_VALUE_SHADOW_RADIUS = 10; private static final int DEFAULT_VALUE_SHADOW_TOP_HEIGHT = 5; private static final int DEFAULT_VALUE_SHADOW_LEFT_HEIGHT = 5; private static final int DEFAULT_VALUE_SHADOW_RIGHT_HEIGHT = 5; private static final int DEFAULT_VALUE_SHADOW_BOTTOM_HEIGHT = 5; private static final int DEFAULT_VALUE_SHADOW_OFFSET_Y = 0; private static final int DEFAULT_VALUE_SHADOW_OFFSET_X = DEFAULT_VALUE_SHADOW_TOP_HEIGHT / 3; private int shadowRound; private int shadowColor; private int shadowCardColor; private int shadowRadius; private int shadowOffsetY; private int shadowOffsetX; private int shadowTopHeight; private int shadowLeftHeight; private int shadowRightHeight; private int shadowBottomHeight; public ShadowViewCard(Context context) { this(context, null); } public ShadowViewCard(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ShadowViewCard(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(context, attrs); } private void initView(Context context, AttributeSet attrs) { TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ShadowViewCard); shadowRound = a.getDimensionPixelSize(R.styleable.ShadowViewCard_shadowRound, DEFAULT_VALUE_SHADOW_ROUND); shadowColor = a.getColor(R.styleable.ShadowViewCard_shadowColor, getResources().getColor(DEFAULT_VALUE_SHADOW_COLOR)); shadowCardColor = a.getColor(R.styleable.ShadowViewCard_shadowCardColor, getResources().getColor(DEFAULT_VALUE_SHADOW_CARD_COLOR)); shadowTopHeight = a.getDimensionPixelSize(R.styleable.ShadowViewCard_shadowTopHeight, dp2px(getContext(), DEFAULT_VALUE_SHADOW_TOP_HEIGHT)); shadowRightHeight = a.getDimensionPixelSize(R.styleable.ShadowViewCard_shadowRightHeight, dp2px(getContext(), DEFAULT_VALUE_SHADOW_RIGHT_HEIGHT)); shadowLeftHeight = a.getDimensionPixelSize(R.styleable.ShadowViewCard_shadowLeftHeight, dp2px(getContext(), DEFAULT_VALUE_SHADOW_LEFT_HEIGHT)); shadowBottomHeight = a.getDimensionPixelSize(R.styleable.ShadowViewCard_shadowBottomHeight, dp2px(getContext(), DEFAULT_VALUE_SHADOW_BOTTOM_HEIGHT)); shadowOffsetY = a.getDimensionPixelSize(R.styleable.ShadowViewCard_shadowOffsetY, dp2px(getContext(), DEFAULT_VALUE_SHADOW_OFFSET_Y)); shadowOffsetX = a.getDimensionPixelSize(R.styleable.ShadowViewCard_shadowOffsetX, dp2px(getContext(), DEFAULT_VALUE_SHADOW_OFFSET_X)); shadowRadius = a.getInteger(R.styleable.ShadowViewCard_shadowRadius, DEFAULT_VALUE_SHADOW_RADIUS); a.recycle(); setPadding(shadowLeftHeight, shadowTopHeight, shadowRightHeight, shadowBottomHeight); setLayerType(LAYER_TYPE_SOFTWARE, null); } public static int dp2px(Context context, float dipValue) { final float scale = context.getResources().getDisplayMetrics().density; return (int) (dipValue * scale + 0.5f); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); } @Override protected void dispatchDraw(Canvas canvas) { Paint shadowPaint = new Paint(); shadowPaint.setColor(Color.WHITE); shadowPaint.setStyle(Paint.Style.FILL); shadowPaint.setAntiAlias(true); float left = shadowLeftHeight; float top = shadowTopHeight; float right = getWidth() - shadowRightHeight; float bottom = getHeight() - shadowBottomHeight; shadowPaint.setShadowLayer(shadowRadius, shadowOffsetX, shadowOffsetX, shadowColor); RectF rectF = new RectF(left, top, right, bottom); canvas.drawRoundRect(rectF, shadowRound, shadowRound, shadowPaint); canvas.save(); super.dispatchDraw(canvas); } }
attr.xml
<declare-styleable name="ShadowViewCard"> <!--圓角度--> <attr name="shadowRound" format="dimension" /> <!--陰影的高度--> <attr name="shadowLeftHeight" format="dimension" /> <attr name="shadowTopHeight" format="dimension" /> <attr name="shadowRightHeight" format="dimension" /> <attr name="shadowBottomHeight" format="dimension" /> <!--上方陰影的偏離度--> <attr name="shadowOffsetY" format="dimension" /> <attr name="shadowOffsetX" format="dimension" /> <!--陰影顏色--> <attr name="shadowCardColor" format="color" /> <attr name="shadowColor" format="color" /> <!--陰影顏色模糊度,越大越模糊--> <attr name="shadowRadius" format="integer" /> </declare-styleable>
color.xml
<color name="shadow_default_color">#1a000000</color> <color name="shadow_card_default_color">#ffffff</color>
有更多其他想法的朋友,歡迎加群:979045005,一起交流技術。吃貨還整理了一份Android高階工程師技術大綱以及一套系統全面而且非常深入的Android進階資料,需要的可以進群,找管理領取。

高階進階技術大綱

Android系統進階資料