在開發過程中常會遇見帶陰影效果的控制元件,通過 SDK 提供的 CardView
和 android:elevation
可以實現,也可以通過 .9
圖實現。但是使用這兩種方法會有一些弊端,比如:不可以控制陰影顏色,如果使用 .9
圖片過多,會增加 APK
安裝檔案的體積。針對以上問題,自己寫了一個為控制元件新增陰影的庫 ---- ShadowLayout
。接下來就 ShadowLayout
展開本文,本文主要分為以下兩個部分:
- 關於
ShadowLayout
的使用; - 關於
ShadowLayout
的原理。
關於 ShadowLayout 的使用
先來看一張使用 ShadowLayout
庫實現的各種陰影的效果圖,如下圖所示:
如上圖所示,通過使用 ShadowLayout
可以控制陰影的顏色、範圍、顯示邊界(上下左右四個邊界)、x 軸和 y 軸的偏移量。
新增依賴
Gradle:
compile 'com.lijiankun24:shadowlayout:1.0.0'複製程式碼
Maven:
<dependency>
<groupId>com.lijiankun24</groupId>
<artifactId>shadowlayout</artifactId>
<version>1.0.0</version>
<type>pom</type>
</dependency>複製程式碼
如何使用
在 xml 中新增如下佈局檔案:
<com.lijiankun24.shadowlayout.ShadowLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="30dp"
app:shadowColor="#66000000"
app:shadowDx="0dp"
app:shadowDy="3dp"
app:shadowRadius="10dp"
app:shadowSide="all">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:contentDescription="@null"
android:src="@mipmap/ic_launcher"/>
</com.lijiankun24.shadowlayout.ShadowLayout>複製程式碼
上面 xml 佈局檔案實現的效果如下圖所示:
如上面 xml 中程式碼顯示的那樣,總共有 5 個自定義屬性,其含義分別如下:
app:shadowColor="#66000000"
控制陰影的顏色,注意:顏色必須帶有透明度的值app:shadowDx="0dp"
控制陰影 x 軸的偏移量app:shadowDy="3dp"
控制陰影 y 軸的偏移量app:shadowRadius="10dp"
控制陰影的範圍app:shadowSide="all|left|right|top|bottom"
控制陰影顯示的邊界,共有五個值
關於 ShadowLayout 的原理
ShadowLayout
的原理其實非常簡單,大概可以分為以下幾步:
- 通過自定義屬性獲取陰影的相關屬性,包括:陰影顏色、陰影範圍大小、陰影顯示邊界、陰影 x 軸和 y 軸的偏移量;
- 在
onLayout()
方法中獲取到陰影應該顯示的範圍,並設定此ShadowLayout
的Padding
值以給陰影的顯示留出空間; - 在
onDraw()
方法中使用Canvas
和Paint
的方法繪製陰影。繪製陰影最重要的兩個方法:
Paint.setShadowLayer(float radius, float dx, float dy, int shadowColor)
設定陰影的大小、顏色、x 軸和 y 軸的偏移量canvas.drawRect(RectF rect, Paint paint)
設定陰影顯示的位置
在 ShadowLayout
庫中只有一個檔案 ---- ShadowLayout.java
,ShadowLayout
是 RelativeLayout
的子類,其原始碼如下所示(有較為詳細的註釋):
/**
* ShadowLayout.java
* <p>
* Created by lijiankun on 17/8/11.
*/
public class ShadowLayout extends RelativeLayout {
public static final int ALL = 0x1111;
public static final int LEFT = 0x0001;
public static final int TOP = 0x0010;
public static final int RIGHT = 0x0100;
public static final int BOTTOM = 0x1000;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private RectF mRectF = new RectF();
/**
* 陰影的顏色
*/
private int mShadowColor = Color.TRANSPARENT;
/**
* 陰影的大小範圍
*/
private float mShadowRadius = 0;
/**
* 陰影 x 軸的偏移量
*/
private float mShadowDx = 0;
/**
* 陰影 y 軸的偏移量
*/
private float mShadowDy = 0;
/**
* 陰影顯示的邊界
*/
private int mShadowSide = ALL;
public ShadowLayout(Context context) {
this(context, null);
}
public ShadowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ShadowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}
/**
* 獲取繪製陰影的位置,併為 ShadowLayout 設定 Padding 以為顯示陰影留出空間
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
float effect = mShadowRadius + dip2px(5);
float rectLeft = 0;
float rectTop = 0;
float rectRight = this.getWidth();
float rectBottom = this.getHeight();
int paddingLeft = 0;
int paddingTop = 0;
int paddingRight = 0;
int paddingBottom = 0;
if (((mShadowSide & LEFT) == LEFT)) {
rectLeft = effect;
paddingLeft = (int) effect;
}
if (((mShadowSide & TOP) == TOP)) {
rectTop = effect;
paddingTop = (int) effect;
}
if (((mShadowSide & RIGHT) == RIGHT)) {
rectRight = this.getWidth() - effect;
paddingRight = (int) effect;
}
if (((mShadowSide & BOTTOM) == BOTTOM)) {
rectBottom = this.getHeight() - effect;
paddingBottom = (int) effect;
}
if (mShadowDy != 0.0f) {
rectBottom = rectBottom - mShadowDy;
paddingBottom = paddingBottom + (int) mShadowDy;
}
if (mShadowDx != 0.0f) {
rectRight = rectRight - mShadowDx;
paddingRight = paddingRight + (int) mShadowDx;
}
mRectF.left = rectLeft;
mRectF.top = rectTop;
mRectF.right = rectRight;
mRectF.bottom = rectBottom;
this.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
}
/**
* 真正繪製陰影的方法
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(mRectF, mPaint);
}
/**
* 讀取設定的陰影的屬性
*
* @param attrs 從其中獲取設定的值
*/
private void init(AttributeSet attrs) {
setLayerType(View.LAYER_TYPE_SOFTWARE, null); // 關閉硬體加速
this.setWillNotDraw(false); // 呼叫此方法後,才會執行 onDraw(Canvas) 方法
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.ShadowLayout);
if (typedArray != null) {
mShadowColor = typedArray.getColor(R.styleable.ShadowLayout_shadowColor,
ContextCompat.getColor(getContext(), android.R.color.black));
mShadowRadius = typedArray.getDimension(R.styleable.ShadowLayout_shadowRadius, dip2px(0));
mShadowDx = typedArray.getDimension(R.styleable.ShadowLayout_shadowDx, dip2px(0));
mShadowDy = typedArray.getDimension(R.styleable.ShadowLayout_shadowDy, dip2px(0));
mShadowSide = typedArray.getInt(R.styleable.ShadowLayout_shadowSide, ALL);
typedArray.recycle();
}
mPaint.setAntiAlias(true);
mPaint.setColor(Color.TRANSPARENT);
mPaint.setShadowLayer(mShadowRadius, mShadowDx, mShadowDy, mShadowColor);
}
/**
* dip2px dp 值轉 px 值
*
* @param dpValue dp 值
* @return px 值
*/
private float dip2px(float dpValue) {
DisplayMetrics dm = getContext().getResources().getDisplayMetrics();
float scale = dm.density;
return (dpValue * scale + 0.5F);
}
}複製程式碼
至此,關於 ShadowLayout
庫的使用方法和原理至此全部介紹完畢,庫在 GitHub 上 ShadowLayout,歡迎 star 和 fork,也歡迎通過下面二維碼下載 APK 體驗,如果有什麼問題歡迎指出。我的工作郵箱:jiankunli24@gmail.com
參考資料: