自己设置Layout,让子View支持圆角属性

作者 : 开心源码 本文共8201个字,预计阅读时间需要21分钟 发布时间: 2022-05-12 共198人阅读

前言

在开发中,圆角和阴影效果是很常用的。实现的方法也很多,比方通过xml自己设置shape,比方通过代码继承drawable,还有通过第三发框架实现。但是使用起来还是有些许不灵活,所以我们通过自己设置子view的属性,而后通过父布局来控制子view的圆角,阴影等属性。

继承ConstraintLayout

开发中复杂的布局基本上都可以通过ConstraintLayout实现,所以我们继承ConstraintLayout实现一个EasyConstraintLayout能够为子view增加圆角和阴影效果。

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中自己设置少量属性

<?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>

重写LayoutParams,读取子View自己设置属性

EasyConstraintLayout内部定义一个静态类LayoutParams继承ConstraintLayout.LayoutParams,而后在构造方法中读取上面自己设置的属性。我们通过裁剪的方式实现圆角效果,因而还有要获取子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();    }}

圆角和阴影实现原理

由于我们是通过父布局控制子view的圆角和阴影行为,所以我们重写drawChild来实现,drawChild之前,先通过paintShadowLayer属性把子View的阴影先画上,这个阴影需要裁剪掉子view自身的大小位置。而后再画子view,并且裁剪圆角部分,最终实现圆角阴影效果。裁剪起初我们想到的是通过canvasclipPath方法实现,但是发现会有很大的锯齿。所以改用paintxfermode来裁剪阴影和子view。

onLayout初始化裁剪信息

EasyConstraintLayout中初始化LayoutParamsDatapaths

  @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        );    }}

drawChild中画阴影,裁剪出圆角

我们在EasyConstraintLayout中初始化paint,并且关闭硬件加速,而后在drawChild中实现阴影逻辑,最终代码如下。

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>

最终效果如下:

项目地址: iamyours/EasyWidgets

读者福利分享

Android开发资料+面试架构资料 免费分享 点击链接 就可领取

《Android架构师必备学习资源免费领取(架构视频+面试专题文档+学习笔记)》

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 自己设置Layout,让子View支持圆角属性

发表回复