可设置阴影颜色的 CardView

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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容