前言
在开发中, 圆角和阴影效果是很常用的. 实现的方法也很多, 比如通过 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 之前, 先通过 paint 的 ShadowLayer 属性把子 View 的阴影先画上, 这个阴影需要裁剪掉子 view 自身的大小位置. 然后再画子 view, 并且裁剪圆角部分, 最终实现圆角阴影效果. 裁剪起初我们想到的是通过 canvas 的 clipPath 方法实现, 但是发现会有很大的锯齿. 所以改用 paint 的 xfermode 来裁剪阴影和子 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 的保存起来, 新增两个属性
- 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>
最终效果如下:
项目地址: https://github.com/iamyours/EasyWidgets
读者福利分享
Android 开发资料 + 面试架构资料 免费分享 点击链接 即可领取
《Android 架构师必备学习资源免费领取 (架构视频 + 面试专题文档 + 学习笔记)》
来源: http://www.jianshu.com/p/c7733583d60a