CardView源碼解析-View陰影

CardView 擴(kuò)展 FrameLayout 類并讓您能夠顯示卡片內(nèi)的信息蹭沛,這些信息在整個(gè)平臺(tái)中擁有一致的呈現(xiàn)方式纳胧。CardView 小部件可擁有陰影和圓角汗销。


如果要使用陰影創(chuàng)建卡片卵慰,請(qǐng)使用 card_view:cardElevation 屬性缩歪。CardView 在 Android 5.0(API 級(jí)別 21)及更高版本中使用真實(shí)高度與動(dòng)態(tài)陰影灵莲,而在早期的 Android 版本中則返回編程陰影實(shí)現(xiàn)

<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 卡片內(nèi)容于邊距的間隔
card_view:contentPaddingBottom 卡片內(nèi)容與底部的邊距
card_view:contentPaddingTop 卡片內(nèi)容與頂部的邊距
card_view:contentPaddingLeft 卡片內(nèi)容與左邊的邊距
card_view:contentPaddingRight 卡片內(nèi)容與右邊的邊距
card_view:contentPaddingStart 卡片內(nèi)容于邊距的間隔起始
card_view:contentPaddingEnd 卡片內(nèi)容于邊距的間隔終止
card_view:cardUseCompatPadding 設(shè)置內(nèi)邊距咖刃,android5.0(代號(hào):Lollipop,API level 21)及以上的版本和之前的版本仍舊具有一樣的計(jì)算方式
card_view:cardPreventConrerOverlap 在android5.0之前的版本中添加內(nèi)邊距苟翻,這個(gè)屬性為了防止內(nèi)容和邊角的重疊

CardView的使用比較簡單的止,網(wǎng)上也有相當(dāng)多的文章可以參考檩坚,本文在此不做過多的闡述。
CardView的兼容性考慮诅福,在不同版本的系統(tǒng)上實(shí)現(xiàn)有差異匾委,大家在使用時(shí)要考慮到這一點(diǎn),我們先看幾個(gè)小Demo氓润。

cardPreventConrerOverlap屬性:

Lollipop以下版本赂乐,cardPreventConrerOverlap = false,不設(shè)置contentPadding,如圖,內(nèi)容和圓角重疊咖气。



Lollipop以下版本挨措,cardPreventConrerOverlap = true,不設(shè)置contentPadding,如圖,添加了額外padding防止內(nèi)容和圓角為重疊崩溪。

cardUseCompatPadding屬性:

為了展示出效果浅役,我將elevation設(shè)置的比較大,測試設(shè)備nexus4 768x1280伶唯。
下圖左側(cè)為Lollipop以下版本觉既,右側(cè)為Lollipop版本,cardUseCompatPadding = false。




下圖左側(cè)為Lollipop以下版本瞪讼,右側(cè)為Lollipop版本钧椰,cardUseCompatPadding = true。


可見符欠,如果想讓Lollipop版本及以上的內(nèi)邊距和Lollipop版本以下相同嫡霞,就需要把該屬性設(shè)置為true.。

但該屬性的影響沒你想的那么簡單背亥。
我們?cè)偻季种刑砑右粋€(gè)控件秒际,cardUseCompatPadding= false悬赏。




這差距夠明顯吧狡汉!那我們?cè)鯓颖WC各個(gè)版本的顯示效果相同呢?設(shè)置cardUseCompatPadding=true闽颇。




這下就ok了盾戴,其實(shí)導(dǎo)致這種問題出現(xiàn)的根本原因是view的陰影效果在不同版本實(shí)現(xiàn)方式存在差異,下文分析源碼時(shí)會(huì)講到兵多。

這個(gè)問題還有一種解決方式尖啡。不設(shè)置cardUseCompatPadding屬性為true,在Lollipop版本以下對(duì)應(yīng)的dimens.xml中填寫cardview的margin = 0dp,在Lollipop版本下(即values-21文件夾)的dimens.xml設(shè)置需要的值剩膘。

如果你想給CardView指定明確的寬高呢衅斩?

什么內(nèi)容區(qū)域大小居然不一樣!這種問題要被測試美眉發(fā)現(xiàn)豈不是太沒面子怠褐,怎么解決呢畏梆?設(shè)置cardUseCompatPadding屬性為true。

同理這這個(gè)問題也可以通過不同系統(tǒng)版本dimens.xml來適配奈懒。

總結(jié)一下

  • CardView通過elevation屬性來設(shè)置view的陰影奠涌,但Lollipop之前的版本是模擬實(shí)現(xiàn),即實(shí)現(xiàn)方式不同磷杏。

  • 因?yàn)椴眉舯容^耗費(fèi)性能溜畅,所以Lollipop之前的版本不對(duì)內(nèi)部View進(jìn)行裁剪,通過添加padding的方式避免內(nèi)部View和圓角重疊极祸。使用setPreventCornerOverlap方法或?qū)?yīng)xml card_view:cardPreventConrerOverlap屬性可更改這一行為慈格,該屬性默認(rèn)為true。

  • Lollipop之前的版本遥金,CardView和內(nèi)容之間添加邊距浴捆,并在該區(qū)域繪制陰影,兩邊的間距為maxCardElevation + (1 - cos45) * cornerRadius汰规,上下的間距為maxCardElevation * 1.5 + (1 - cos45) * cornerRadius汤功。

  • 因?yàn)閜adding屬性被用來做偏移繪制陰影,所以不能使用CardView的padding屬性溜哮,如果想設(shè)置CardView和其子View之間的邊距滔金,可使用setContentPadding(int, int, int, int)方法或?qū)?yīng)的xml屬性色解。

  • 如果對(duì)CardView設(shè)置了明確的尺寸,因?yàn)殛幱暗木壒什鸵穑鋬?nèi)容區(qū)域在Lollipop版本和之前的版本上顯示不同科阎,你可以通過不同系統(tǒng)版本使用不同資源值或設(shè)置useCompatPadding屬性為true的方式來避免此問題。

  • 通過setCardElevation(float)以兼容的方式設(shè)置CardView的elevation,CardView會(huì)使用Lollipop下或之前版本下的elevation API忿族,進(jìn)而改變陰影的尺寸锣笨。為防止改變陰影尺寸時(shí),view發(fā)生移動(dòng)道批,陰影大小不會(huì)超過MaxCardElevation错英,如果想在CardView初始化后動(dòng)態(tài)改變陰影大小,應(yīng)使用setMaxCardElevation(float)方法隆豹。

解剖源碼椭岩。

結(jié)構(gòu)圖如下:


CardView內(nèi)部根據(jù)不同版本系統(tǒng)實(shí)例化對(duì)應(yīng)的CardViewImpl對(duì)象,CardViewImpl對(duì)象通過CardViewDelegate對(duì)象與CardView交互璃赡。

CardView的靜態(tài)代碼塊中根據(jù)系統(tǒng)版本實(shí)例化對(duì)應(yīng)的實(shí)現(xiàn)判哥。

 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();
    }

這里調(diào)用了initStatic(),API21中即CardViewApi21類中是空實(shí)現(xiàn)碉考,API17中實(shí)現(xiàn)如下(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之前的版本實(shí)現(xiàn)如下(CardViewGingerbread):

public void initStatic() {
        //使用7步繪制操作來繪制出圓角矩形塌计,在API17之前的版本此種方式要比canvas.drawRoundRect快,
        //因?yàn)锳PI 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和之前版本的陰影實(shí)現(xiàn)差異主要在這里,因?yàn)樾蕟栴}侯谁,API17之前的版本使用分步繪制測方式繪制圓角矩形锌仅。

CardView的構(gòu)造器中會(huì)調(diào)用initialize(...),該方法中主要拿到各屬性,然后調(diào)用具體實(shí)現(xiàn)的初始化方法良蒸。

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 {
            // 沒有設(shè)置背景則從當(dāng)前主題中提取
            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之前版本如何實(shí)現(xiàn)陰影?
  2. API19及以上版本如何實(shí)現(xiàn)View裁切嫩痰?
  3. API19及之后版本如何實(shí)現(xiàn)陰影剿吻?
  4. API19之前版本cardPreventConrerOverlap屬性的影響?
  5. API19及以上版本受cardUseCompatPadding屬性的影響串纺?
  6. 為什么陰影在在x軸方向和y軸方向發(fā)生了位移丽旅,而不是均勻分布在view四周?

問題1:API19之前版本如何實(shí)現(xiàn)陰影纺棺?

接下來我們先看API19之前是怎么實(shí)現(xiàn)陰影的(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類來實(shí)現(xiàn)的。
我們來看看來它的陰影是由RoundRectDrawableWithShadow類來實(shí)現(xiàn)的onDraw,第一次調(diào)用要走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其實(shí)就是maxElevation,此處確定了cardview的邊界,上下左右都進(jìn)行了偏移米丘,空出來的區(qū)域是為了繪制陰影剑令。

  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之前陰影的實(shí)現(xiàn)是由canvas+path+RadialGradient繪制角陰影,canvas+path+LinearGradient繪制邊陰影。

問題2:API19及以上版本如何實(shí)現(xiàn)View裁切堕扶?

進(jìn)入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);
    }

這里實(shí)例化了RoundRectDrawable作為cardview的背景。RoundRectDrawable是用來繪制背景圓角矩形稍算,
cardView.getCardView()拿到了CardView對(duì)象典尾,前面我們說了CardView是繼承自FrameLayout的,所以CardView即是ViewGroup也是View,view.setClipToOutline(true)是什么意思呢邪蛔?

android5.0之后允許自定義視圖陰影與輪廓
視圖的背景可繪制對(duì)象的邊界將決定其陰影的默認(rèn)形狀急黎。輪廓代表圖形對(duì)象的外形并定義觸摸反饋的波紋區(qū)域。

下面舉一個(gè)以背景可繪制對(duì)象定義的視圖示例:

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

背景可繪制對(duì)象被定義為一個(gè)擁有圓角的矩形:

<!-- 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>

視圖將投射一個(gè)帶有圓角的陰影侧到,因?yàn)楸尘翱衫L制對(duì)象將定義視圖的輪廓。 如果提供一個(gè)自定義輪廓淤击,則此輪廓將替換視圖陰影的默認(rèn)形狀匠抗。

如果要為代碼中的視圖定義自定義輪廓:
擴(kuò)展 ViewOutlineProvider 類別。
替代 getOutline() 方法污抬。
利用 View.setOutlineProvider() 方法向您的視圖指定新的輪廓提供程序汞贸。
您可使用 Outline 類別中的方法創(chuàng)建帶有圓角的橢圓形和矩形輪廓。視圖的默認(rèn)輪廓提供程序?qū)囊晥D背景取得輪廓印机。 如果要防止視圖投射陰影矢腻,請(qǐng)將其輪廓提供程序設(shè)置為 null。

裁剪視圖
裁剪視圖讓您能夠輕松改變視圖形狀射赛。您可以裁剪視圖多柑,以便與其他設(shè)計(jì)元素保持一致,也可以根據(jù)用戶輸入改變視圖形狀楣责。您可使用 View.setClipToOutline() 方法或 android:clipToOutline 屬性將視圖裁剪至其輪廓區(qū)域竣灌。 由 Outline.canClip() 方法所決定,僅有矩形秆麸、圓形和圓角矩形輪廓支持裁剪初嘹。

如果要將視圖裁剪至可繪制對(duì)象的形狀,請(qǐng)將可繪制對(duì)象設(shè)置為視圖背景(如上所示)并調(diào)用 View.setClipToOutline() 方法沮趣。

問題3: API19及之后版本如何實(shí)現(xiàn)陰影屯烦?

CardViewApi21的initialize方法中調(diào)用了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)是個(gè)native方法,由此可見android5.0開始所有的view都可以顯示陰影,而且是根據(jù)elevation屬性直接有native方法來實(shí)現(xiàn)驻龟。

android5.0開始因?yàn)榧尤肓薓aterial Design,Material Design 為 UI 元素引入高度甸箱,為View加上了Z屬性。

由 Z 屬性所表示的視圖高度將決定其陰影的視覺外觀:擁有較高 Z 值的視圖將投射更大且更柔和的陰影迅脐。 擁有較高 Z 值的視圖將擋住擁有較低 Z 值的視圖芍殖;不過視圖的 Z 值并不影響視圖的大小。

視圖的 Z 值包含兩個(gè)組件:

高度:靜態(tài)組件谴蔑。
轉(zhuǎn)換:用于動(dòng)畫的動(dòng)態(tài)組件豌骏。
Z = elevation + translationZ

所以影響View陰影的因素有兩個(gè)elevation和translationZ.
在 Material Design Guidelines 中有建議卡片、按鈕這類元素觸摸時(shí)應(yīng)當(dāng)有一個(gè)浮起的效果隐锭,也就是增大 Z 軸位移,我們?cè)趺磳?shí)現(xiàn)這個(gè)效果呢窃躲?


只需要借助 Lollipop 的一個(gè)新屬性 android:stateListAnimator,創(chuàng)建一個(gè) TranslationZ 的變換動(dòng)畫放在 /res/anim,自己取一個(gè)名(如 touch_raise.xml)钦睡,加入以下內(nèi)容:

<?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 復(fù)制多一份到 /res/layout-v21蒂窒,然后在新的那份 XML 的 CardView 中加入屬性 android:stateListAnimator="@anim/touch_raise"。這樣荞怒,你的卡片按住時(shí)就會(huì)有浮起(陰影加深)的效果了洒琢。
至于波紋效果只需要給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);
    }

一路跟蹤下來荧嵌,發(fā)現(xiàn)關(guān)鍵點(diǎn)在RoundRectDrawableWithShadow中呛踊,addPaddingForCorners即為傳過來的preventCornerOverlap,當(dāng)preventCornerOverlap為true時(shí)啦撮,內(nèi)邊距增加了(1 - COS_45) * cornerRadius)谭网,這樣CardView的子View就不會(huì)和圓角重疊了。

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);
        }
    }

進(jìn)入CardViewApi21。

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

最后跟蹤到RoundRectDrawable,這里的mInsetForPadding就是cardUseCompatPadding屬性的值聘鳞,當(dāng)cardUseCompatPadding屬性為true時(shí)薄辅,會(huì)設(shè)置內(nèi)邊距,calculateVerticalPadding和calculateHorizontalPadding方法是RoundRectDrawableWithShadow的靜態(tài)方法抠璃,如此5.0和之前版本就具有相同的內(nèi)邊距計(jì)算方式了站楚。

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方法也可以看到,如果不設(shè)置cardUseCompatPadding,其陰影內(nèi)邊距為0搏嗡,這也就解釋了前文中的現(xiàn)象窿春。

@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軸方向發(fā)生了位移拉一,而不是均勻分布在view四周?

在RoundRectDrawableWithShadow的draw方法中旧乞,我們看到蔚润,在繪制陰影前,畫布向y軸正方向進(jìn)行了位移尺栖,這就使得陰影的方向發(fā)生了變化嫡纠。

 @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);
    }

如果項(xiàng)目的設(shè)計(jì)符合Material Design,那最好延赌,如果設(shè)計(jì)有一天讓你實(shí)現(xiàn)四周帶相同尺寸陰影的效果呢除盏?我們也知道怎么做了吧!
這里我把實(shí)現(xiàn)方式放到Github上了挫以,有需要?dú)g迎關(guān)注者蠕。

參考資料:
https://developer.android.com/training/material/lists-cards.html#Dependencies
http://www.reibang.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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市掐松,隨后出現(xiàn)的幾起案子踱侣,更是在濱河造成了極大的恐慌,老刑警劉巖大磺,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件抡句,死亡現(xiàn)場離奇詭異,居然都是意外死亡量没,警方通過查閱死者的電腦和手機(jī)玉转,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來殴蹄,“玉大人,你說我怎么就攤上這事猾担∠疲” “怎么了?”我有些...
    開封第一講書人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵绑嘹,是天一觀的道長稽荧。 經(jīng)常有香客問我,道長工腋,這世上最難降的妖魔是什么姨丈? 我笑而不...
    開封第一講書人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮擅腰,結(jié)果婚禮上蟋恬,老公的妹妹穿的比我還像新娘。我一直安慰自己趁冈,他們只是感情好歼争,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開白布拜马。 她就那樣靜靜地躺著,像睡著了一般沐绒。 火紅的嫁衣襯著肌膚如雪俩莽。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,727評(píng)論 1 305
  • 那天乔遮,我揣著相機(jī)與錄音扮超,去河邊找鬼。 笑死蹋肮,一個(gè)胖子當(dāng)著我的面吹牛出刷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播括尸,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼巷蚪,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了濒翻?” 一聲冷哼從身側(cè)響起屁柏,我...
    開封第一講書人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎有送,沒想到半個(gè)月后淌喻,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡雀摘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年裸删,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片阵赠。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡涯塔,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出清蚀,到底是詐尸還是另有隱情匕荸,我是刑警寧澤,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布枷邪,位于F島的核電站榛搔,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏东揣。R本人自食惡果不足惜践惑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望嘶卧。 院中可真熱鬧尔觉,春花似錦、人聲如沸脸候。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至泵额,卻和暖如春配深,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背嫁盲。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來泰國打工篓叶, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人羞秤。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓缸托,卻偏偏與公主長得像,于是被迫代替她去往敵國和親瘾蛋。 傳聞我的和親對(duì)象是個(gè)殘疾皇子俐镐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容