在開發中,圓角和陰影效果是很經常使用的。實現的方法也不少,好比經過xml
自定義shape
,好比經過代碼繼承drawable
,還有經過第三發框架實現。可是使用起來仍是有些許不靈活,因此咱們經過自定義子view的屬性,而後經過父佈局來控制子view
的圓角,陰影等屬性。java
開發中複雜的佈局基本上均可以經過ConstraintLayout
實現,因此咱們繼承ConstraintLayout
實現一個EasyConstraintLayout
可以爲子view添加圓角和陰影效果。android
public class EasyConstraintLayout extends ConstraintLayout { public EasyConstraintLayout(Context context, AttributeSet attrs) { super(context, attrs); } @Override public LinearLayout.LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p instanceof LayoutParams; } }
重寫了兩個方法,咱們要用這些方法實現子view
自定義屬性的讀取,在此以前要在xml
中自定義一些屬性git
<?xml version="1.0" encoding="utf-8"?> <resources> <!--爲了方便擴展其餘layout,定義在外層,命名以layout_開頭,不然lint會報紅警告--> <attr name="layout_radius" format="dimension" /> <attr name="layout_shadowColor" format="color" /> <attr name="layout_shadowEvaluation" format="dimension" /> <attr name="layout_shadowDx" format="dimension" /> <attr name="layout_shadowDy" format="dimension" /> <!--用統一一個EasyLayout,用於封裝讀取自定義屬性--> <declare-styleable name="EasyLayout"> <attr name="layout_radius" /> <attr name="layout_shadowColor" /> <attr name="layout_shadowEvaluation" /> <attr name="layout_shadowDx" /> <attr name="layout_shadowDy" /> </declare-styleable> <!--和EasyLayout屬性列表同樣,可是命名要以XXX_Layout格式,這樣開發工具會提示自定義屬性--> <declare-styleable name="EasyConstraintLayout_Layout"> <attr name="layout_radius" /> <attr name="layout_shadowColor" /> <attr name="layout_shadowEvaluation" /> <attr name="layout_shadowDx" /> <attr name="layout_shadowDy" /> </declare-styleable> </resources>
在EasyConstraintLayout
內部定義一個靜態類LayoutParams
繼承ConstraintLayout.LayoutParams
,而後在構造方法中讀取上面自定義的屬性。咱們經過裁剪的方式實現圓角效果,所以還有要獲取子view的位置和大小。github
static class LayoutParams extends ConstraintLayout.LayoutParams implements EasyLayoutParams{ private LayoutParamsData data; public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); data = new LayoutParamsData(c, attrs); } @Override public LayoutParamsData getData() { return data; } }
public interface EasyLayoutParams { LayoutParamsData getData(); }
public class LayoutParamsData { int radius; int shadowColor; int shadowDx; int shadowDy; int shadowEvaluation; public LayoutParamsData(Context context, AttributeSet attrs) { TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.EasyLayout); radius = a.getDimensionPixelOffset(R.styleable.EasyLayout_layout_radius, 0); shadowDx = a.getDimensionPixelOffset(R.styleable.EasyLayout_layout_shadowDx, 0); shadowDy = a.getDimensionPixelOffset(R.styleable.EasyLayout_layout_shadowDy, 0); shadowColor = a.getColor(R.styleable.EasyLayout_layout_shadowColor, 0x99999999); shadowEvaluation = a.getDimensionPixelOffset(R.styleable.EasyLayout_layout_shadowEvaluation, 0); a.recycle(); } }
由於咱們是經過父佈局控制子view的圓角和陰影行爲,因此咱們重寫drawChild
來實現,drawChild
以前,先經過paint
的ShadowLayer
屬性把子View的陰影先畫上,這個陰影須要裁剪掉子view自身的大小位置。而後再畫子view,而且裁剪圓角部分,最終實現圓角陰影效果。裁剪起初咱們想到的是經過canvas
的clipPath
方法實現,可是發現會有很大的鋸齒。因此改用paint
的xfermode
來裁剪陰影和子view。面試
在EasyConstraintLayout
中初始化LayoutParamsData
的paths
canvas
@Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); for (int i = 0, size = getChildCount(); i < size; i++) { View v = getChildAt(i); ViewGroup.LayoutParams lp = v.getLayoutParams(); if(lp instanceof EasyLayoutParams){ EasyLayoutParams elp = (EasyLayoutParams) lp; elp.getData().initPaths(v); } } }
在LayoutParamsData
中將裁剪陰影的path
和裁剪子view的保存起來,新增兩個屬性架構
public class LayoutParamsData { Path widgetPath; Path clipPath; boolean needClip; boolean hasShadow; public LayoutParamsData(Context context, AttributeSet attrs) { … needClip = radius > 0; hasShadow = shadowEvaluation > 0; } public void initPaths(View v) { widgetPath = new Path(); clipPath = new Path(); clipPath.addRect(widgetRect, Path.Direction.CCW); clipPath.addRoundRect( widgetRect, radius, radius, Path.Direction.CW ); widgetPath.addRoundRect( widgetRect, radius, radius, Path.Direction.CW ); } }
咱們在EasyConstraintLayout
中初始化paint
,而且關閉硬件加速,而後在drawChild
中實現陰影邏輯,最終代碼以下。app
public class EasyConstraintLayout extends ConstraintLayout { private Paint shadowPaint; private Paint clipPaint; public EasyConstraintLayout(Context context, AttributeSet attrs) { super(context, attrs); shadowPaint = new Paint(); shadowPaint.setAntiAlias(true); shadowPaint.setDither(true); shadowPaint.setFilterBitmap(true); shadowPaint.setStyle(Paint.Style.FILL); clipPaint = new Paint(); clipPaint.setAntiAlias(true); clipPaint.setDither(true); clipPaint.setFilterBitmap(true); clipPaint.setStyle(Paint.Style.FILL); setLayerType(View.LAYER_TYPE_SOFTWARE, null); } @Override public ConstraintLayout.LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p instanceof LayoutParams; } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); for (int i = 0, size = getChildCount(); i < size; i++) { View v = getChildAt(i); ViewGroup.LayoutParams lp = v.getLayoutParams(); if (lp instanceof EasyLayoutParams) { EasyLayoutParams elp = (EasyLayoutParams) lp; elp.getData().initPaths(v); } } } @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { ViewGroup.LayoutParams lp = child.getLayoutParams(); boolean ret = false; if (lp instanceof EasyLayoutParams) { EasyLayoutParams elp = (EasyLayoutParams) lp; LayoutParamsData data = elp.getData(); if (isInEditMode()) {//預覽模式採用裁剪 canvas.save(); canvas.clipPath(data.widgetPath); ret = super.drawChild(canvas, child, drawingTime); canvas.restore(); return ret; } if (!data.hasShadow && !data.needClip) return super.drawChild(canvas, child, drawingTime); //爲解決鋸齒問題,正式環境採用xfermode if (data.hasShadow) { int count = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG); shadowPaint.setShadowLayer(data.shadowEvaluation, data.shadowDx, data.shadowDy, data.shadowColor); shadowPaint.setColor(data.shadowColor); canvas.drawPath(data.widgetPath, shadowPaint); shadowPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); shadowPaint.setColor(Color.WHITE); canvas.drawPath(data.widgetPath, shadowPaint); shadowPaint.setXfermode(null); canvas.restoreToCount(count); } if (data.needClip) { int count = canvas.saveLayer(child.getLeft(), child.getTop(), child.getRight(), child.getBottom(), null, Canvas.ALL_SAVE_FLAG); ret = super.drawChild(canvas, child, drawingTime); clipPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); clipPaint.setColor(Color.WHITE); canvas.drawPath(data.clipPath, clipPaint); clipPaint.setXfermode(null); canvas.restoreToCount(count); } } return ret; } static class LayoutParams extends ConstraintLayout.LayoutParams implements EasyLayoutParams { private LayoutParamsData data; public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); data = new LayoutParamsData(c, attrs); } @Override public LayoutParamsData getData() { return data; } } }
<?xml version="1.0" encoding="utf-8"?> <io.github.iamyours.easylayout.EasyConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <View android:id="@+id/v_back" android:layout_width="match_parent" android:layout_height="150dp" android:layout_margin="10dp" android:background="#fff" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_radius="4dp" app:layout_shadowColor="#3ccc" app:layout_shadowEvaluation="15dp" /> <ImageView android:id="@+id/iv_head" android:layout_width="80dp" android:layout_height="80dp" android:layout_gravity="center_horizontal" android:layout_marginLeft="10dp" android:background="#eee" app:layout_constraintBottom_toBottomOf="@id/v_back" app:layout_constraintLeft_toLeftOf="@id/v_back" app:layout_constraintTop_toTopOf="@id/v_back" app:layout_radius="40dp" app:layout_shadowColor="#5f00" app:layout_shadowEvaluation="8dp" /> <View android:layout_width="200dp" android:layout_height="200dp" android:layout_marginTop="30dp" android:background="#ccc" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/v_back" app:layout_radius="30dp" app:layout_shadowColor="#8f0f" app:layout_shadowDx="4dp" app:layout_shadowDy="4dp" app:layout_shadowEvaluation="10dp" /> </io.github.iamyours.easylayout.EasyConstraintLayout>
最終效果以下:框架
項目地址: https://github.com/iamyours/E...ide