CardView源码解析-View阴影

CardView 扩展 FrameLayout 类并让您能够显示卡片内的信息,这些信息在整个平台中拥有一致的呈现方式。CardView 小部件可拥有阴影和圆角。


如果要使用阴影创建卡片,请使用 card_view:cardElevation 属性。CardView 在 Android 5.0(API 级别 21)及更高版本中使用真实高度与动态阴影,而在早期的 Android 版本中则返回编程阴影实现

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <android.support.v7.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="16dp">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">
            <ImageView
                android:layout_width="match_parent"
                android:layout_height="160dp"
                android:scaleType="centerCrop"
                android:src="@drawable/balon" />
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                android:padding="16dp">
                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="8dp"
                    android:text="@string/card_title"
                    android:textColor="#000"
                    android:textSize="18sp" />
                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="@string/card_content"
                    android:textColor="#555" />
            </LinearLayout>
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content">
                <Button
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/action_share"
                    android:theme="@style/PrimaryFlatButton" />
                <Button
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/action_explore"
                    android:theme="@style/PrimaryFlatButton" />
            </LinearLayout>
        </LinearLayout>
    </android.support.v7.widget.CardView>
</RelativeLayout>

CardView的常用属性:

属性 作用
card_view:cardElevation 阴影的大小
card_view:cardMaxElevation 阴影最大高度
card_view:cardBackgroundColor 卡片的背景色
card_view:cardCornerRadius 卡片的圆角大小
card_view:contentPadding 卡片内容于边距的间隔
card_view:contentPaddingBottom 卡片内容与底部的边距
card_view:contentPaddingTop 卡片内容与顶部的边距
card_view:contentPaddingLeft 卡片内容与左边的边距
card_view:contentPaddingRight 卡片内容与右边的边距
card_view:contentPaddingStart 卡片内容于边距的间隔起始
card_view:contentPaddingEnd 卡片内容于边距的间隔终止
card_view:cardUseCompatPadding 设置内边距,android5.0(代号:Lollipop,API level 21)及以上的版本和之前的版本仍旧具有一样的计算方式
card_view:cardPreventConrerOverlap 在android5.0之前的版本中添加内边距,这个属性为了防止内容和边角的重叠

CardView的使用比较简单,网上也有相当多的文章可以参考,本文在此不做过多的阐述。
CardView的兼容性考虑,在不同版本的系统上实现有差异,大家在使用时要考虑到这一点,我们先看几个小Demo。

cardPreventConrerOverlap属性:

Lollipop以下版本,cardPreventConrerOverlap = false,不设置contentPadding,如图,内容和圆角重叠。



Lollipop以下版本,cardPreventConrerOverlap = true,不设置contentPadding,如图,添加了额外padding防止内容和圆角为重叠。

cardUseCompatPadding属性:

为了展示出效果,我将elevation设置的比较大,测试设备nexus4 768x1280。
下图左侧为Lollipop以下版本,右侧为Lollipop版本,cardUseCompatPadding = false。




下图左侧为Lollipop以下版本,右侧为Lollipop版本,cardUseCompatPadding = true。


可见,如果想让Lollipop版本及以上的内边距和Lollipop版本以下相同,就需要把该属性设置为true.。

但该属性的影响没你想的那么简单。
我们再往布局中添加一个控件,cardUseCompatPadding= false。




这差距够明显吧!那我们怎样保证各个版本的显示效果相同呢?设置cardUseCompatPadding=true。




这下就ok了,其实导致这种问题出现的根本原因是view的阴影效果在不同版本实现方式存在差异,下文分析源码时会讲到。

这个问题还有一种解决方式。不设置cardUseCompatPadding属性为true,在Lollipop版本以下对应的dimens.xml中填写cardview的margin = 0dp,在Lollipop版本下(即values-21文件夹)的dimens.xml设置需要的值。

如果你想给CardView指定明确的宽高呢?

什么内容区域大小居然不一样!这种问题要被测试美眉发现岂不是太没面子,怎么解决呢?设置cardUseCompatPadding属性为true。

同理这这个问题也可以通过不同系统版本dimens.xml来适配。

总结一下

  • CardView通过elevation属性来设置view的阴影,但Lollipop之前的版本是模拟实现,即实现方式不同。

  • 因为裁剪比较耗费性能,所以Lollipop之前的版本不对内部View进行裁剪,通过添加padding的方式避免内部View和圆角重叠。使用setPreventCornerOverlap方法或对应xml card_view:cardPreventConrerOverlap属性可更改这一行为,该属性默认为true。

  • Lollipop之前的版本,CardView和内容之间添加边距,并在该区域绘制阴影,两边的间距为maxCardElevation + (1 - cos45) * cornerRadius,上下的间距为maxCardElevation * 1.5 + (1 - cos45) * cornerRadius。

  • 因为padding属性被用来做偏移绘制阴影,所以不能使用CardView的padding属性,如果想设置CardView和其子View之间的边距,可使用setContentPadding(int, int, int, int)方法或对应的xml属性。

  • 如果对CardView设置了明确的尺寸,因为阴影的缘故,其内容区域在Lollipop版本和之前的版本上显示不同,你可以通过不同系统版本使用不同资源值或设置useCompatPadding属性为true的方式来避免此问题。

  • 通过setCardElevation(float)以兼容的方式设置CardView的elevation,CardView会使用Lollipop下或之前版本下的elevation API,进而改变阴影的尺寸。为防止改变阴影尺寸时,view发生移动,阴影大小不会超过MaxCardElevation,如果想在CardView初始化后动态改变阴影大小,应使用setMaxCardElevation(float)方法。

解剖源码。

结构图如下:


CardView内部根据不同版本系统实例化对应的CardViewImpl对象,CardViewImpl对象通过CardViewDelegate对象与CardView交互。

CardView的静态代码块中根据系统版本实例化对应的实现。

 static {
        if (Build.VERSION.SDK_INT >= 21) {
            IMPL = new CardViewApi21();
        } else if (Build.VERSION.SDK_INT >= 17) {
            IMPL = new CardViewJellybeanMr1();
        } else {
            IMPL = new CardViewGingerbread();
        }
        IMPL.initStatic();
    }

这里调用了initStatic(),API21中即CardViewApi21类中是空实现,API17中实现如下(CardViewJellybeanMr1):

public void initStatic() {
        RoundRectDrawableWithShadow.sRoundRectHelper
                = new RoundRectDrawableWithShadow.RoundRectHelper() {
            @Override
            public void drawRoundRect(Canvas canvas, RectF bounds, float cornerRadius,
                    Paint paint) {
                canvas.drawRoundRect(bounds, cornerRadius, cornerRadius, paint);
            }
        };
    }

API17之前的版本实现如下(CardViewGingerbread):

public void initStatic() {
        //使用7步绘制操作来绘制出圆角矩形,在API17之前的版本此种方式要比canvas.drawRoundRect快,
        //因为API 11-16使用了alpha蒙版纹理去绘制
        RoundRectDrawableWithShadow.sRoundRectHelper =
                new RoundRectDrawableWithShadow.RoundRectHelper() {
            @Override
            public void drawRoundRect(Canvas canvas, RectF bounds, float cornerRadius,
                    Paint paint) {
                final float twoRadius = cornerRadius * 2;
                final float innerWidth = bounds.width() - twoRadius - 1;
                final float innerHeight = bounds.height() - twoRadius - 1;
                if (cornerRadius >= 1f) {
                    // increment corner radius to account for half pixels.
                    float roundedCornerRadius = cornerRadius + .5f;
                    sCornerRect.set(-roundedCornerRadius, -roundedCornerRadius, roundedCornerRadius,
                            roundedCornerRadius);
                    int saved = canvas.save();
                    canvas.translate(bounds.left + roundedCornerRadius,
                            bounds.top + roundedCornerRadius);
                    canvas.drawArc(sCornerRect, 180, 90, true, paint);
                    canvas.translate(innerWidth, 0);
                    canvas.rotate(90);
                    canvas.drawArc(sCornerRect, 180, 90, true, paint);
                    canvas.translate(innerHeight, 0);
                    canvas.rotate(90);
                    canvas.drawArc(sCornerRect, 180, 90, true, paint);
                    canvas.translate(innerWidth, 0);
                    canvas.rotate(90);
                    canvas.drawArc(sCornerRect, 180, 90, true, paint);
                    canvas.restoreToCount(saved);
                    //绘制上下两部分
                    canvas.drawRect(bounds.left + roundedCornerRadius - 1f, bounds.top,
                            bounds.right - roundedCornerRadius + 1f,
                            bounds.top + roundedCornerRadius, paint);

                    canvas.drawRect(bounds.left + roundedCornerRadius - 1f,
                            bounds.bottom - roundedCornerRadius,
                            bounds.right - roundedCornerRadius + 1f, bounds.bottom, paint);
                }
                // 绘制中间部分
                canvas.drawRect(bounds.left, bounds.top + cornerRadius,
                        bounds.right, bounds.bottom - cornerRadius , paint);
            }
        };
    }

API17和之前版本的阴影实现差异主要在这里,因为效率问题,API17之前的版本使用分步绘制测方式绘制圆角矩形。

CardView的构造器中会调用initialize(...),该方法中主要拿到各属性,然后调用具体实现的初始化方法。

private void initialize(Context context, AttributeSet attrs, int defStyleAttr) {
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CardView, defStyleAttr,
                R.style.CardView);
        ColorStateList backgroundColor;
        if (a.hasValue(R.styleable.CardView_cardBackgroundColor)) {
            backgroundColor = a.getColorStateList(R.styleable.CardView_cardBackgroundColor);
        } else {
            // 没有设置背景则从当前主题中提取
            final TypedArray aa = getContext().obtainStyledAttributes(COLOR_BACKGROUND_ATTR);
            final int themeColorBackground = aa.getColor(0, 0);
            aa.recycle();

            //若主题中的colorBackground是浅色,使用cardview_light_background,否则使用cardview_dark_background
            final float[] hsv = new float[3];
            Color.colorToHSV(themeColorBackground, hsv);
            backgroundColor = ColorStateList.valueOf(hsv[2] > 0.5f
                    ? getResources().getColor(R.color.cardview_light_background)
                    : getResources().getColor(R.color.cardview_dark_background));
        }
        float radius = a.getDimension(R.styleable.CardView_cardCornerRadius, 0);
        ...
        mUserSetMinHeight = a.getDimensionPixelSize(R.styleable.CardView_android_minHeight, 0);
        a.recycle();

        IMPL.initialize(mCardViewDelegate, context, backgroundColor, radius,
                elevation, maxElevation);
    }

接下来我们带着问题来看源码。

  1. API19之前版本如何实现阴影?
  2. API19及以上版本如何实现View裁切?
  3. API19及之后版本如何实现阴影?
  4. API19之前版本cardPreventConrerOverlap属性的影响?
  5. API19及以上版本受cardUseCompatPadding属性的影响?
  6. 为什么阴影在在x轴方向和y轴方向发生了位移,而不是均匀分布在view四周?

问题1:API19之前版本如何实现阴影?

接下来我们先看API19之前是怎么实现阴影的(CardViewGingerbread类)。

 @Override
    public void initialize(CardViewDelegate cardView, Context context,
            ColorStateList backgroundColor, float radius, float elevation, float maxElevation) {
        RoundRectDrawableWithShadow background = createBackground(context, backgroundColor, radius,
                elevation, maxElevation);
        background.setAddPaddingForCorners(cardView.getPreventCornerOverlap());
        cardView.setCardBackground(background);
        updatePadding(cardView);
    }

看来它的阴影是由RoundRectDrawableWithShadow类来实现的。
我们来看看来它的阴影是由RoundRectDrawableWithShadow类来实现的onDraw,第一次调用要走buildComponents方法。

public void draw(Canvas canvas) {
        if (mDirty) {
            buildComponents(getBounds());
            mDirty = false;
        }
        canvas.translate(0, mRawShadowSize / 2);
        drawShadow(canvas);
        canvas.translate(0, -mRawShadowSize / 2);
        sRoundRectHelper.drawRoundRect(canvas, mCardBounds, mCornerRadius, mPaint);
    }

buildComponents方法中,mRawMaxShadowSize其实就是maxElevation,此处确定了cardview的边界,上下左右都进行了偏移,空出来的区域是为了绘制阴影。

  private void buildComponents(Rect bounds) {
        // Card is offset SHADOW_MULTIPLIER * maxShadowSize to account for the shadow shift.
        // We could have different top-bottom offsets to avoid extra gap above but in that case
        // center aligning Views inside the CardView would be problematic.
        final float verticalOffset = mRawMaxShadowSize * SHADOW_MULTIPLIER;
        mCardBounds.set(bounds.left + mRawMaxShadowSize, bounds.top+verticalOffset,
                bounds.right - mRawMaxShadowSize, bounds.bottom - verticalOffset);

        buildShadowCorners();
    }

buildShadowCorners方法中初始化了绘制边阴影和角阴影的path。

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);
        mCornerShadowPaint.setShader(new RadialGradient(0, 0, mCornerRadius + mShadowSize,
                new int[]{mShadowStartColor, mShadowStartColor, mShadowEndColor},
                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[]{mShadowStartColor, mShadowStartColor, mShadowEndColor},
                new float[]{0f, .5f, 1f}, Shader.TileMode.CLAMP));
        mEdgeShadowPaint.setAntiAlias(false);
    }

接下来看drawShadow方法,这里基本的canvas操作。

private void drawShadow(Canvas canvas) {
        final float edgeShadowTop = -mCornerRadius - mShadowSize;
        final float inset = mCornerRadius + mInsetShadow + mRawShadowSize / 2;
        final boolean drawHorizontalEdges = mCardBounds.width() - 2 * inset > 0;
        final boolean drawVerticalEdges = mCardBounds.height() - 2 * inset > 0;
        // LT
        int saved = canvas.save();
        canvas.translate(mCardBounds.left + inset, mCardBounds.top + inset);
        canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
        if (drawHorizontalEdges) {
            canvas.drawRect(0, edgeShadowTop,
                    mCardBounds.width() - 2 * inset, -mCornerRadius,
                    mEdgeShadowPaint);
        }
        canvas.restoreToCount(saved);
        // RB
        saved = canvas.save();
        canvas.translate(mCardBounds.right - inset, mCardBounds.bottom - inset);
        canvas.rotate(180f);
        canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
        if (drawHorizontalEdges) {
            canvas.drawRect(0, edgeShadowTop,
                    mCardBounds.width() - 2 * inset, -mCornerRadius + mShadowSize,
                    mEdgeShadowPaint);
        }
        canvas.restoreToCount(saved);
        // LB
        saved = canvas.save();
        canvas.translate(mCardBounds.left + inset, mCardBounds.bottom - inset);
        canvas.rotate(270f);
        canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
        if (drawVerticalEdges) {
            canvas.drawRect(0, edgeShadowTop,
                    mCardBounds.height() - 2 * inset, -mCornerRadius, mEdgeShadowPaint);
        }
        canvas.restoreToCount(saved);
        // RT
        saved = canvas.save();
        canvas.translate(mCardBounds.right - inset, mCardBounds.top + inset);
        canvas.rotate(90f);
        canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
        if (drawVerticalEdges) {
            canvas.drawRect(0, edgeShadowTop,
                    mCardBounds.height() - 2 * inset, -mCornerRadius, mEdgeShadowPaint);
        }
        canvas.restoreToCount(saved);
    }

可见API19之前阴影的实现是由canvas+path+RadialGradient绘制角阴影,canvas+path+LinearGradient绘制边阴影。

问题2:API19及以上版本如何实现View裁切?

进入CardViewApi21类,

@Override
    public void initialize(CardViewDelegate cardView, Context context,
                           ColorStateList backgroundColor, float radius, float elevation, float maxElevation) {
        final RoundRectDrawable background = new RoundRectDrawable(backgroundColor, radius);
        cardView.setCardBackground(background);

        View view = cardView.getCardView();
        view.setClipToOutline(true);
        view.setElevation(elevation);
        setMaxElevation(cardView, maxElevation);
    }

这里实例化了RoundRectDrawable作为cardview的背景。RoundRectDrawable是用来绘制背景圆角矩形,
cardView.getCardView()拿到了CardView对象,前面我们说了CardView是继承自FrameLayout的,所以CardView即是ViewGroup也是View,view.setClipToOutline(true)是什么意思呢?

android5.0之后允许自定义视图阴影与轮廓
视图的背景可绘制对象的边界将决定其阴影的默认形状。轮廓代表图形对象的外形并定义触摸反馈的波纹区域。

下面举一个以背景可绘制对象定义的视图示例:

<TextView
    android:id="@+id/myview"
    ...
    android:elevation="2dp"
    android:background="@drawable/myrect" />

背景可绘制对象被定义为一个拥有圆角的矩形:

<!-- res/drawable/myrect.xml -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle">
    <solid android:color="#42000000" />
    <corners android:radius="5dp" />
</shape>

视图将投射一个带有圆角的阴影,因为背景可绘制对象将定义视图的轮廓。 如果提供一个自定义轮廓,则此轮廓将替换视图阴影的默认形状。

如果要为代码中的视图定义自定义轮廓:
扩展 ViewOutlineProvider 类别。
替代 getOutline() 方法。
利用 View.setOutlineProvider() 方法向您的视图指定新的轮廓提供程序。
您可使用 Outline 类别中的方法创建带有圆角的椭圆形和矩形轮廓。视图的默认轮廓提供程序将从视图背景取得轮廓。 如果要防止视图投射阴影,请将其轮廓提供程序设置为 null。

裁剪视图
裁剪视图让您能够轻松改变视图形状。您可以裁剪视图,以便与其他设计元素保持一致,也可以根据用户输入改变视图形状。您可使用 View.setClipToOutline() 方法或 android:clipToOutline 属性将视图裁剪至其轮廓区域。 由 Outline.canClip() 方法所决定,仅有矩形、圆形和圆角矩形轮廓支持裁剪。

如果要将视图裁剪至可绘制对象的形状,请将可绘制对象设置为视图背景(如上所示)并调用 View.setClipToOutline() 方法。

问题3: API19及之后版本如何实现阴影?

CardViewApi21的initialize方法中调用了view.setElevation(elevation),

public void setElevation(float elevation) {
        if (elevation != getElevation()) {
            invalidateViewProperty(true, false);
            mRenderNode.setElevation(elevation);
            invalidateViewProperty(false, true);

            invalidateParentIfNeededAndWasQuickRejected();
        }
    }

再看mRenderNode.setElevation(elevation);

     public boolean setElevation(float lift) {
        return nSetElevation(mNativeRenderNode, lift);
    }

nSetElevation(mNativeRenderNode, lift)是个native方法,由此可见android5.0开始所有的view都可以显示阴影,而且是根据elevation属性直接有native方法来实现。

android5.0开始因为加入了Material Design,Material Design 为 UI 元素引入高度,为View加上了Z属性。

由 Z 属性所表示的视图高度将决定其阴影的视觉外观:拥有较高 Z 值的视图将投射更大且更柔和的阴影。 拥有较高 Z 值的视图将挡住拥有较低 Z 值的视图;不过视图的 Z 值并不影响视图的大小。

视图的 Z 值包含两个组件:

高度:静态组件。
转换:用于动画的动态组件。
Z = elevation + translationZ

所以影响View阴影的因素有两个elevation和translationZ.
在 Material Design Guidelines 中有建议卡片、按钮这类元素触摸时应当有一个浮起的效果,也就是增大 Z 轴位移,我们怎么实现这个效果呢?


只需要借助 Lollipop 的一个新属性 android:stateListAnimator,创建一个 TranslationZ 的变换动画放在 /res/anim,自己取一个名(如 touch_raise.xml),加入以下内容:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_enabled="true" android:state_pressed="true">
        <objectAnimator
        android:duration="@android:integer/config_shortAnimTime"
        android:propertyName="translationZ"
        android:valueTo="@dimen/touch_raise"
        android:valueType="floatType" />
    </item>
    <item>
        <objectAnimator
        android:duration="@android:integer/config_shortAnimTime"
        android:propertyName="translationZ"
        android:valueTo="0dp"
        android:valueType="floatType" />
    </item>
</selector>

然后为你需要添加效果的 CardView(其他 View 同理)所在的 Layout XML 复制多一份到 /res/layout-v21,然后在新的那份 XML 的 CardView 中加入属性 android:stateListAnimator="@anim/touch_raise"。这样,你的卡片按住时就会有浮起(阴影加深)的效果了。
至于波纹效果只需要给CardView加上android:foreground="?attr/selectableItemBackground" 属性即可。

问题4:API19之前版本cardPreventConrerOverlap属性的影响?

CardView的setPreventCornerOverlap方法。

  public void setPreventCornerOverlap(boolean preventCornerOverlap) {
        if (preventCornerOverlap != mPreventCornerOverlap) {
            mPreventCornerOverlap = preventCornerOverlap;
            IMPL.onPreventCornerOverlapChanged(mCardViewDelegate);
        }
    }

然后看CardViewGingerbread的onPreventCornerOverlapChanged方法。

 @Override
    public void onPreventCornerOverlapChanged(CardViewDelegate cardView) {
        getShadowBackground(cardView).setAddPaddingForCorners(cardView.getPreventCornerOverlap());
        updatePadding(cardView);
    }

一路跟踪下来,发现关键点在RoundRectDrawableWithShadow中,addPaddingForCorners即为传过来的preventCornerOverlap,当preventCornerOverlap为true时,内边距增加了(1 - COS_45) * cornerRadius),这样CardView的子View就不会和圆角重叠了。

static float calculateVerticalPadding(float maxShadowSize, float cornerRadius,
                                          boolean addPaddingForCorners) {
        if (addPaddingForCorners) {
            return (float) (maxShadowSize * SHADOW_MULTIPLIER + (1 - COS_45) * cornerRadius);
        } else {
            return maxShadowSize * SHADOW_MULTIPLIER;
        }
    }

问题5 API19及以上版本受cardUseCompatPadding属性的影响?

CardView的setUseCompatPadding方法。

  public void setUseCompatPadding(boolean useCompatPadding) {
        if (mCompatPadding != useCompatPadding) {
            mCompatPadding = useCompatPadding;
            IMPL.onCompatPaddingChanged(mCardViewDelegate);
        }
    }

进入CardViewApi21。

 @Override
    public void onCompatPaddingChanged(CardViewDelegate cardView) {
        setMaxElevation(cardView, getMaxElevation(cardView));
    }

最后跟踪到RoundRectDrawable,这里的mInsetForPadding就是cardUseCompatPadding属性的值,当cardUseCompatPadding属性为true时,会设置内边距,calculateVerticalPadding和calculateHorizontalPadding方法是RoundRectDrawableWithShadow的静态方法,如此5.0和之前版本就具有相同的内边距计算方式了。

private void updateBounds(Rect bounds) {
        if (bounds == null) {
            bounds = getBounds();
        }
        mBoundsF.set(bounds.left, bounds.top, bounds.right, bounds.bottom);
        mBoundsI.set(bounds);
        if (mInsetForPadding) {
            float vInset = calculateVerticalPadding(mPadding, mRadius, mInsetForRadius);
            float hInset = calculateHorizontalPadding(mPadding, mRadius, mInsetForRadius);
            mBoundsI.inset((int) Math.ceil(hInset), (int) Math.ceil(vInset));
            // to make sure they have same bounds.
            mBoundsF.set(mBoundsI);
        }
    }

在CardViewApi21的updatePadding方法也可以看到,如果不设置cardUseCompatPadding,其阴影内边距为0,这也就解释了前文中的现象。

@Override
    public void updatePadding(CardViewDelegate cardView) {
        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);
    }

问题6 为什么阴影在在x轴方向和y轴方向发生了位移,而不是均匀分布在view四周?

在RoundRectDrawableWithShadow的draw方法中,我们看到,在绘制阴影前,画布向y轴正方向进行了位移,这就使得阴影的方向发生了变化。

 @Override
    public void draw(Canvas canvas) {
        if (mDirty) {
            buildComponents(getBounds());
            mDirty = false;
        }
        canvas.translate(0, mRawShadowSize / 2);
        drawShadow(canvas);
        canvas.translate(0, -mRawShadowSize / 2);
        sRoundRectHelper.drawRoundRect(canvas, mCardBounds, mCornerRadius, mPaint);
    }

如果项目的设计符合Material Design,那最好,如果设计有一天让你实现四周带相同尺寸阴影的效果呢?我们也知道怎么做了吧!
这里我把实现方式放到Github上了,有需要欢迎关注。

参考资料:
https://developer.android.com/training/material/lists-cards.html#Dependencies
http://www.jianshu.com/p/33b1d21d6ba6
https://developer.android.com/training/material/shadows-clipping.html#Shadows
https://android.jlelse.eu/android-card-view-edb905e67cd6
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/1025/3621.html

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

推荐阅读更多精彩内容