前言
在開(kāi)發(fā)中蚁袭,圓角和陰影效果是很常用的。實(shí)現(xiàn)的方法也很多石咬,比如通過(guò)xml自定義shape揩悄,比如通過(guò)代碼繼承drawable,還有通過(guò)第三發(fā)框架實(shí)現(xiàn)鬼悠。但是使用起來(lái)還是有些許不靈活虏束,所以我們通過(guò)自定義子view的屬性,然后通過(guò)父布局來(lái)控制子view的圓角厦章,陰影等屬性镇匀。
繼承ConstraintLayout
開(kāi)發(fā)中復(fù)雜的布局基本上都可以通過(guò)ConstraintLayout實(shí)現(xiàn),所以我們繼承ConstraintLayout實(shí)現(xiàn)一個(gè)EasyConstraintLayout能夠?yàn)樽觱iew添加圓角和陰影效果袜啃。
public class EasyConstraintLayout extends ConstraintLayout {
public EasyConstraintLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public ConstraintLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
}
重寫(xiě)了兩個(gè)方法汗侵,我們要用這些方法實(shí)現(xiàn)子view自定義屬性的讀取,在此之前要在xml中自定義一些屬性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--為了方便擴(kuò)展其他layout群发,定義在外層晰韵,命名以layout_開(kāi)頭,否則lint會(huì)報(bào)紅警告-->
<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" />
<!--用統(tǒng)一一個(gè)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格式,這樣開(kāi)發(fā)工具會(huì)提示自定義屬性-->
<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>
重寫(xiě)LayoutParams起愈,讀取子View自定義屬性
在EasyConstraintLayout內(nèi)部定義一個(gè)靜態(tài)類(lèi)LayoutParams繼承ConstraintLayout.LayoutParams只恨,然后在構(gòu)造方法中讀取上面自定義的屬性译仗。我們通過(guò)裁剪的方式實(shí)現(xiàn)圓角效果,因此還有要獲取子view的位置和大小官觅。
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();
}
}
圓角和陰影實(shí)現(xiàn)原理
因?yàn)槲覀兪峭ㄟ^(guò)父布局控制子view的圓角和陰影行為纵菌,所以我們重寫(xiě)drawChild來(lái)實(shí)現(xiàn),drawChild之前休涤,先通過(guò)paint的ShadowLayer屬性把子View的陰影先畫(huà)上咱圆,這個(gè)陰影需要裁剪掉子view自身的大小位置。然后再畫(huà)子view功氨,并且裁剪圓角部分序苏,最終實(shí)現(xiàn)圓角陰影效果。
裁剪起初我們想到的是通過(guò)canvas的clipPath方法實(shí)現(xiàn)捷凄,但是發(fā)現(xiàn)會(huì)有很大的鋸齒杠览。所以改用paint的xfermode來(lái)裁剪陰影和子view。
onLayout初始化裁剪信息
在EasyConstraintLayout中初始化LayoutParamsData的paths
@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的保存起來(lái)纵势,新增兩個(gè)屬性
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
);
}
}
drawChild中畫(huà)陰影踱阿,裁剪出圓角
我們?cè)贓asyConstraintLayout中初始化paint,并且關(guān)閉硬件加速钦铁,然后在drawChild中實(shí)現(xiàn)陰影邏輯软舌,最終代碼如下。
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()) {//預(yù)覽模式采用裁剪
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);
//為解決鋸齒問(wèn)題牛曹,正式環(huán)境采用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>
最終效果如下: