Android開發中陰影效果的實現

Liberuman發表於2018-04-26

背景

隨著這幾年UI風格的不斷升級,陰影已經成了很多APP設計中的不可或缺的元素。但Android在這方面卻沒有比較好的實現方式。

這裡有總結的一篇關於Android陰影效果的文章,比較全面,值得一看。聊聊 Material Design 裡,陰影的那些事兒!

上面這篇文章對Android中各版本的陰影實現都進行了說明,這裡就不再細說了。雖然提供的方式很多,但是卻有很大的侷限性,具體表現在以下兩方面:

  • 無法改變陰影的顏色;
  • 存在相容性問題;

雖然也可以按照作者提供的方式,使用Fab或CardView實現陰影的原理來實現,但相對比較麻煩,這裡我們提供一個簡單的實現方案。

實現思想

為View新增陰影,其實就是為View提供一個有陰影的背景而已,所以有2中實現方式:

  • 重寫View的onDraw()方法;
  • 自定義Drawable;

第一種明顯不合理,我們不可能重寫每個需要設定陰影的View的onDraw(),所以這裡選擇自定義Drwable(通過設定Paint的ShadowLayer)來實現。需要注意的是:這種方式實現的陰影,其目標View需要關閉硬體加速。

view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
複製程式碼

需求點

  • 可設定陰影顏色,圓角,面積,偏移量;
  • 可設定View的背景形狀,顏色,圓角;

實現

原始碼地址:ShadowDrawable

public class ShadowDrawable extends Drawable {

	private Paint mPaint;
	private int mShadowRadius;  // 陰影圓角
	private int mShape;         // 背景形狀
	private int mShapeRadius;   // 背景圓角
	private int mOffsetX;       // 陰影的水平偏移量
	private int mOffsetY;       // 陰影的垂直偏移量
	private int mBgColor[];     // 背景顏色
	private RectF mRect;

	public final static int SHAPE_ROUND = 1;    // 表示圓角矩形
	public final static int SHAPE_CIRCLE = 2;   // 表示圓

	private ShadowDrawable(int shape, int[] bgColor, int shapeRadius, int shadowColor, int shadowRadius, int offsetX, int offsetY) {
		this.mShape = shape;
		this.mBgColor = bgColor;
		this.mShapeRadius = shapeRadius;
		this.mShadowRadius = shadowRadius;
		this.mOffsetX = offsetX;
		this.mOffsetY = offsetY;
		mPaint = new Paint();
		mPaint.setColor(Color.TRANSPARENT);
		mPaint.setAntiAlias(true);
		mPaint.setShadowLayer(shadowRadius, offsetX, offsetY, shadowColor);
	    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP));
	}

	@Override
	public void setBounds(int left, int top, int right, int bottom) {
		super.setBounds(left, top, right, bottom);
		mRect = new RectF(left + mShadowRadius - mOffsetX, top + mShadowRadius - mOffsetY, right - mShadowRadius - mOffsetX,
				bottom - mShadowRadius - mOffsetY);
	}

	@Override
	public void draw(@NonNull Canvas canvas) {
		if (mShape == SHAPE_ROUND) {
			canvas.drawRoundRect(mRect, mShapeRadius, mShapeRadius, mPaint);
			Paint newPaint = new Paint();

			if (mBgColor != null) {
				if (mBgColor.length == 1) {
					newPaint.setColor(mBgColor[0]);
				} else {
					newPaint.setShader(new LinearGradient(mRect.left, mRect.height() / 2, mRect.right, mRect.height() / 2, mBgColor,
							null, Shader.TileMode.CLAMP));
				}
			}
			newPaint.setAntiAlias(true);
			canvas.drawRoundRect(mRect, mShapeRadius, mShapeRadius, newPaint);
		} else {
			canvas.drawCircle(mRect.centerX(), mRect.centerY(), Math.min(mRect.width(), mRect.height())/ 2, mPaint);
		}
	}

	@Override
	public void setAlpha(int alpha) {
		mPaint.setAlpha(alpha);
	}

	@Override
	public void setColorFilter(@Nullable ColorFilter colorFilter) {
		mPaint.setColorFilter(colorFilter);
	}

	@Override
	public int getOpacity() {
		return PixelFormat.TRANSLUCENT;
	}
}
複製程式碼

由於提供的屬性比較多,為了便於使用,提供了Builder的鏈式建立方式,同時提供了常用的幾個static方法,設定陰影只需一行程式碼即可,具體檢視ShadowDrawable.java

public static void setShadowDrawable(View view, int shapeRadius, int shadowColor, int shadowRadius, int offsetX, int offsetY) {
	ShadowDrawable drawable = new ShadowDrawable.Builder()
			.setShapeRadius(shapeRadius)
			.setShadowColor(shadowColor)
			.setShadowRadius(shadowRadius)
			.setOffsetX(offsetX)
			.setOffsetY(offsetY)
			.builder();
	view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
	ViewCompat.setBackground(view, drawable);
}
複製程式碼

例項效果

image

注意點

  • 設定陰影的顏色時,需要帶有透明度(即"#XXXXXXXX"的形式,如50%的黑色["#80000000"]),而不能使用純色;
  • 上面提供的這種實現方式,陰影部分總是作為View的一部分而存在的,所以在使用時,需要為陰影留出相對應的padding, 才會讓陰影顯示出來;

總結

由於Android系統並沒有提供完美的解決方案,即便使用5.0以上提供的elevation或translationZ屬性,對於大面積的陰影,看起來也比較生硬, 所以在Android開發中,對於陰影的處理,還是應該分情況來對待,大面積的陰影強烈推薦使用9Patch圖來解決,對於小控制元件的陰影,可使用以上的方案。

相關文章