CardView原始碼解析-View陰影

ietf發表於2017-08-14

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上了,有需要歡迎關注。

參考資料:
developer.android.com/training/ma…
www.jianshu.com/p/33b1d21d6…
developer.android.com/training/ma…
android.jlelse.eu/android-car…
www.jcodecraeer.com/a/anzhuokai…

相關文章