可设置阴影颜色的 CardView

作者: WangJie0822 | 来源:发表于2018-10-10 15:50 被阅读96次
    image

    需求由来

    最近项目中来了新的需求,一大堆卡片式布局,还有不同的阴影颜色,甚至不同的状态下颜色还不一样,UI 给的切图各种错位,而 Google 的 CardView 是无法设置阴影颜色的,我能怎么办,我也很绝望啊(:з」∠)
    没办法,百度了一堆都没有找到解决方案,最后借鉴了各位大神的思路,才有了这篇文章。

    实现思路

    怎么实现?当然是参照 Google 的 CardView 啦,毕竟是亲生的,各种实现优化都是非常棒的,然后就是在 CardView 的基础上,给他添加上设置阴影颜色的功能,很简单的是吧。

    开始干活啦

    • 那么先新建一个项目,然后把 CardView 目录下的所有 类文件、资源文件 都复制到新建的项目中。

      屏幕快照 2018-10-10 10.19.58.png
    • 这样,这个 CardView 就已经可以使用了,和原生的一模一样,同样不能设置阴影颜色,这当然不是我们需要的。

    • 我们的目标是让 CardView 能够支持设置阴影颜色,那么首先我们就来探查一下 CardView 的颜色资源文件。

      屏幕快照 2018-10-10 10.23.16.png
        <color name="cardview_shadow_end_color">#03000000</color>
        <color name="cardview_shadow_start_color">#37000000</color>
    
    • 这是什么?阴影的开始颜色和结束颜色?这么简单就解决了吗?我们可以尝试修改这两个颜色值,然而,并没有效果(O_O)? 这是为什么?

    • 我们来查看这两个资源在哪些地方被使用。


      屏幕快照 2018-10-10 10.30.23.png
    • 这个 RoundRectDrawableWithShadow 是一个集成 Drawable 类,我们在查看一下这个类在哪里被使用。

      屏幕快照 2018-10-10 10.35.53.png
    • 他在 CardViewBaseImpl 中被创建,和这个类似的还有 CardViewApi17ImplCardViewApi21Impl,其中 CardViewApi17Impl 是继承 CardViewBaseImpl 的,我们接下来再看看这两个类是在哪里使用的。

      屏幕快照 2018-10-10 10.39.21.png
    • 我们在 CardView 中发现了他,嗯、、好像是 API 小于21才会使用,我们用一个API 19的模拟器试试。

      屏幕快照 2018-10-10 10.45.11.png
    • 我们成功了,阴影的颜色被改变了,看样子之前之所以没有效果是因为模拟器的API大于等于21了,那么在高版本上我们改如何实现这种效果呢?

    • 首先,我们来看一下,使用代码设置 CardView 高度时进行的操作。

        public void setCardElevation(float elevation) {
            IMPL.setElevation(mCardViewDelegate, elevation);
        }
    
    • 从上面的步骤我们可以知道,当 API 低于21时,CardView 使用 CardViewBaseImplCardViewApi17Impl 来处理的,高于或等于21时使用 CardViewApi21Impl 来处理。这里面有什么区别呢?
        // CardViewBaseImpl
        @Override
        public void setElevation(CardViewDelegate cardView, float elevation) {
            getShadowBackground(cardView).setShadowSize(elevation);
        }
        // CardViewApi21Impl
        @Override
        public void setElevation(CardViewDelegate cardView, float elevation) {
            cardView.getCardView().setElevation(elevation);
        }
    
    • 我们可以看到,当 API 高于或等于21时,使用的是从API21开始才有的Elevation属性设置阴影效果的,而低于21时是通过Drawable来绘制阴影效果。弄清楚了这些,我们就可以开始给 CardView 的阴影添加颜色啦。

    实现可设置阴影颜色的 CardView

    • 首先,我们需要给 CardView 添加两条属性,用来设置阴影的开始颜色和结束颜色。
        <attr name="cardShadowColorStart" format="color" />
        <attr name="cardShadowColorEnd" format="color" />
    
    • 然后,在 CardView 的构造方法里面获取属性。
        ColorStateList shadowColorStart = a.getColorStateList(R.styleable.CardView_cardShadowColorStart);
        ColorStateList shaodwColorEnd = a.getColorStateList(R.styleable.CardView_cardShadowColorEnd);
    
    • 这里为了支持状态选择器,使用 ColorStateList
    • CardView 的构造方法最后会调用 CardViewImplinitialize() 方法进行初始化,因此在 initialize() 方法中添加 两个参数。
    void initialize(CardViewDelegate cardView, Context context, ColorStateList backgroundColor,
                        float radius, float elevation, float maxElevation, ColorStateList shadowColorStart, ColorStateList shadowColorEnd);
    
    • 修改 RoundRectDrawableWithShadow 构造方法,添加阴影颜色参数,并进行处理。
        // 阴影颜色默认是 int 类型的颜色值,要修改成 ColorStateList
        private final ColorStateList mShadowStartColor;
    
        private final ColorStateList mShadowEndColor;
        
        RoundRectDrawableWithShadow(Resources resources, ColorStateList backgroundColor, float radius,
                                    float shadowSize, float maxShadowSize, ColorStateList shadowColorStart, ColorStateList shadowColorEnd) {
            // 如果没有设置阴影颜色,使用默认颜色
            if (shadowColorStart == null) {
                mShadowStartColor = ColorStateList.valueOf(resources.getColor(R.color.cardview_shadow_start_color));
            } else {
                mShadowStartColor = shadowColorStart;
            }
            if (shadowColorEnd == null) {
                mShadowEndColor = ColorStateList.valueOf(resources.getColor(R.color.cardview_shadow_end_color));
            } else {
                mShadowEndColor = shadowColorEnd;
            }
            mInsetShadow = resources.getDimensionPixelSize(R.dimen.cardview_compat_inset_shadow);
            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
            setBackground(backgroundColor);
            mCornerShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
            mCornerShadowPaint.setStyle(Paint.Style.FILL);
            mCornerRadius = (int) (radius + .5f);
            mCardBounds = new RectF();
            mEdgeShadowPaint = new Paint(mCornerShadowPaint);
            mEdgeShadowPaint.setAntiAlias(false);
            setShadowSize(shadowSize, maxShadowSize);
        }
        
        private void buildShadowCorners() {
            RectF innerBounds = new RectF(-mCornerRadius, -mCornerRadius, mCornerRadius, mCornerRadius);
            RectF outerBounds = new RectF(innerBounds);
            outerBounds.inset(-mShadowSize, -mShadowSize);
    
            if (mCornerShadowPath == null) {
                mCornerShadowPath = new Path();
            } else {
                mCornerShadowPath.reset();
            }
            mCornerShadowPath.setFillType(Path.FillType.EVEN_ODD);
            mCornerShadowPath.moveTo(-mCornerRadius, 0);
            mCornerShadowPath.rLineTo(-mShadowSize, 0);
            // outer arc
            mCornerShadowPath.arcTo(outerBounds, 180f, 90f, false);
            // inner arc
            mCornerShadowPath.arcTo(innerBounds, 270f, -90f, false);
            mCornerShadowPath.close();
            float startRatio = mCornerRadius / (mCornerRadius + mShadowSize);
            
            // 获取当前状态下的阴影颜色
            int starColor = mShadowStartColor.getColorForState(getState(), mShadowStartColor.getDefaultColor());
            int endColor = mShadowEndColor.getColorForState(getState(), mShadowEndColor.getDefaultColor());
            // 设置阴影颜色
            mCornerShadowPaint.setShader(new RadialGradient(0, 0, mCornerRadius + mShadowSize,
                    new int[]{starColor, starColor, endColor},
                    new float[]{0f, startRatio, 1f},
                    Shader.TileMode.CLAMP));
    
            // we offset the content shadowSize/2 pixels up to make it more realistic.
            // this is why edge shadow shader has some extra space
            // When drawing bottom edge shadow, we use that extra space.
            mEdgeShadowPaint.setShader(new LinearGradient(0, -mCornerRadius + mShadowSize, 0,
                    -mCornerRadius - mShadowSize,
                    new int[]{starColor, starColor, endColor},
                    new float[]{0f, .5f, 1f}, Shader.TileMode.CLAMP));
            mEdgeShadowPaint.setAntiAlias(false);
        }
    
    • 好了,这样就可以直接使用属性配置阴影颜色了,当然,还是只能在低版本使用。
        <cn.wj.android.colorcardview.CardView
            android:layout_width="200dp"
            android:layout_height="50dp"
            app:cardPreventCornerOverlap="true"
            app:cardBackgroundColor="#069ff1"
            app:cardShadowColorStart="#2dfd0000"
            app:cardShadowColorEnd="#03fd0000"
            app:cardUseCompatPadding="true"
            app:cardElevation="8dp" />
    

    适配高版本

    • 经过上面的步骤之后,我们的 CardView 已经可以通过属性设置阴影颜色了,同时支持状态选择器,但是,这些效果仅仅在API低于21时有效。那么接下来我们来处理高版本的阴影颜色。
    • 我们知道,高版本中,CardView 的阴影效果是通过 CardViewApi21Impl 处理的,其内部是通过 API21 才加入的 Elevation 属性设置的,而 Elevation 这个属性,Google 并没有提供任何接口来对其阴影颜色进行设置。
    • 我的解决思路是,既然高版本的 Elevation 没有提供方法,那么我们就使用低版本的方案处理。


      屏幕快照 2018-10-10 11.37.54.png
    • 我们可以看到,CardViewApi21Impl 是直接实现了 CardViewImpl 接口的,而低版本的实现是在 CardViewBaseImpl 中的,CardViewApi17Impl 继承 CardViewBaseImpl,那么我们可以让 CardViewApi21Impl 直接继承 CardViewApi17Impl
    • 同时进行判断,如果自定义了阴影颜色,那么就是用父类的实现,即低版本的实现,否则,依旧使用高版本的实现。
    @RequiresApi(21)
    class CardViewApi21Impl extends CardViewApi17Impl {
    
        // 标记 - 是否使用低版本实现
        private boolean useLower = false;
    
        @Override
        public void initialize(CardViewDelegate cardView, Context context,
                               ColorStateList backgroundColor, float radius, float elevation, float maxElevation,
                               ColorStateList shadowColorStart, ColorStateList shadowColorEnd) {
    
            // 没有自定义阴影颜色,不使用低版本实现
            if (shadowColorStart == null && shadowColorEnd == null) {
                useLower = false;
    
                final RoundRectDrawable background = new RoundRectDrawable(backgroundColor, radius);
                cardView.setCardBackground(background);
    
                View view = cardView.getCardView();
                view.setClipToOutline(true);
                view.setElevation(elevation);
                setMaxElevation(cardView, maxElevation);
            } else {
                // 配置了自定义颜色,使用低版本实现
                useLower = true;
                super.initialize(cardView, context, backgroundColor, radius, elevation, maxElevation, shadowColorStart, shadowColorEnd);
            }
        }
    
        @Override
        public void setRadius(CardViewDelegate cardView, float radius) {
            if (useLower) {
                super.setRadius(cardView, radius);
            } else {
                getCardBackground(cardView).setRadius(radius);
            }
        }
    
        @Override
        public void setMaxElevation(CardViewDelegate cardView, float maxElevation) {
            if (useLower) {
                super.setMaxElevation(cardView, maxElevation);
            } else {
                getCardBackground(cardView).setPadding(maxElevation,
                        cardView.getUseCompatPadding(), cardView.getPreventCornerOverlap());
                updatePadding(cardView);
            }
        }
    
        @Override
        public float getMaxElevation(CardViewDelegate cardView) {
            if (useLower) {
                return super.getMaxElevation(cardView);
            } else {
                return getCardBackground(cardView).getPadding();
            }
        }
    
        @Override
        public float getMinWidth(CardViewDelegate cardView) {
            if (useLower) {
                return super.getMinWidth(cardView);
            } else {
                return getRadius(cardView) * 2;
            }
        }
    
        @Override
        public float getMinHeight(CardViewDelegate cardView) {
            if (useLower) {
                return super.getMinHeight(cardView);
            } else {
                return getRadius(cardView) * 2;
            }
        }
    
        @Override
        public float getRadius(CardViewDelegate cardView) {
            if (useLower) {
                return super.getRadius(cardView);
            } else {
                return getCardBackground(cardView).getRadius();
            }
        }
    
        @Override
        public void setElevation(CardViewDelegate cardView, float elevation) {
            if (useLower) {
                super.setElevation(cardView, elevation);
            } else {
                cardView.getCardView().setElevation(elevation);
            }
        }
    
        @Override
        public float getElevation(CardViewDelegate cardView) {
            if (useLower) {
                return super.getElevation(cardView);
            } else {
                return cardView.getCardView().getElevation();
            }
        }
    
        @Override
        public void updatePadding(CardViewDelegate cardView) {
            if (useLower) {
                super.updatePadding(cardView);
            } else {
                if (!cardView.getUseCompatPadding()) {
                    cardView.setShadowPadding(0, 0, 0, 0);
                    return;
                }
                float elevation = getMaxElevation(cardView);
                final float radius = getRadius(cardView);
                int hPadding = (int) Math.ceil(RoundRectDrawableWithShadow
                        .calculateHorizontalPadding(elevation, radius, cardView.getPreventCornerOverlap()));
                int vPadding = (int) Math.ceil(RoundRectDrawableWithShadow
                        .calculateVerticalPadding(elevation, radius, cardView.getPreventCornerOverlap()));
                cardView.setShadowPadding(hPadding, vPadding, hPadding, vPadding);
            }
        }
    
        @Override
        public void onCompatPaddingChanged(CardViewDelegate cardView) {
            if (useLower) {
                super.onCompatPaddingChanged(cardView);
            } else {
                setMaxElevation(cardView, getMaxElevation(cardView));
            }
        }
    
        @Override
        public void onPreventCornerOverlapChanged(CardViewDelegate cardView) {
            if (useLower) {
                super.onPreventCornerOverlapChanged(cardView);
            } else {
                setMaxElevation(cardView, getMaxElevation(cardView));
            }
        }
    
        @Override
        public void setBackgroundColor(CardViewDelegate cardView, @Nullable ColorStateList color) {
            if (useLower) {
                super.setBackgroundColor(cardView, color);
            } else {
                getCardBackground(cardView).setColor(color);
            }
        }
    
        @Override
        public ColorStateList getBackgroundColor(CardViewDelegate cardView) {
            if (useLower) {
                return super.getBackgroundColor(cardView);
            } else {
                return getCardBackground(cardView).getColor();
            }
        }
    
        private RoundRectDrawable getCardBackground(CardViewDelegate cardView) {
            return ((RoundRectDrawable) cardView.getCardBackground());
        }
    }
    
    • 到这里,可以设置阴影颜色的 CardView 就已经完成了,我们来看看效果
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"
        tools:context=".MainActivity">
    
        <cn.wj.android.colorcardview.CardView
            android:id="@+id/cv1"
            android:layout_width="200dp"
            android:layout_height="100dp"
            app:cardBackgroundColor="@color/app_selector_card"
            app:cardElevation="8dp"
            app:cardPreventCornerOverlap="true"
            app:cardShadowColorEnd="@color/app_selector_shadow_end"
            app:cardShadowColorStart="@color/app_selector_shadow_start"
            app:cardUseCompatPadding="true" />
    
        <android.support.v7.widget.CardView
            android:id="@+id/cv2"
            android:layout_width="200dp"
            android:layout_height="100dp"
            app:cardBackgroundColor="@color/app_selector_card"
            app:cardElevation="8dp"
            app:cardPreventCornerOverlap="true"
            app:cardUseCompatPadding="true" />
    
    </LinearLayout>
    
    class MainActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            cv1.setOnClickListener { onClick(it) }
            cv2.setOnClickListener { onClick(it) }
        }
    
        fun onClick(v: View) {
            v.isSelected = !v.isSelected
        }
    }
    
    2018-10-10 14.49.23.gif

    最后

    • 嗯嗯,就这样,一个能够自定义阴影颜色的 CardView 就完成了。
    • 这里给上项目地址,所有代码都在这里 MyCardView

    相关文章

      网友评论

      • 有点健忘:写的真好,思路过程都很清晰,赞一个。
        对了,改成低版本的实现方法,那cardview的圆角还有效果吗
        WangJie0822:圆角都是有的,只是阴影的样式不一样

      本文标题:可设置阴影颜色的 CardView

      本文链接:https://www.haomeiwen.com/subject/ybouaftx.html