1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.support.v7.widget; 18 19 import android.content.Context; 20 import android.content.res.ColorStateList; 21 import android.content.res.TypedArray; 22 import android.graphics.Color; 23 import android.graphics.Rect; 24 import android.graphics.drawable.Drawable; 25 import android.os.Build; 26 import android.support.annotation.ColorInt; 27 import android.support.annotation.Nullable; 28 import android.support.v7.cardview.R; 29 import android.util.AttributeSet; 30 import android.view.View; 31 import android.widget.FrameLayout; 32 33 /** 34 * A FrameLayout with a rounded corner background and shadow. 35 * <p> 36 * CardView uses <code>elevation</code> property on Lollipop for shadows and falls back to a 37 * custom emulated shadow implementation on older platforms. 38 * <p> 39 * Due to expensive nature of rounded corner clipping, on platforms before Lollipop, CardView does 40 * not clip its children that intersect with rounded corners. Instead, it adds padding to avoid such 41 * intersection (See {@link #setPreventCornerOverlap(boolean)} to change this behavior). 42 * <p> 43 * Before Lollipop, CardView adds padding to its content and draws shadows to that area. This 44 * padding amount is equal to <code>maxCardElevation + (1 - cos45) * cornerRadius</code> on the 45 * sides and <code>maxCardElevation * 1.5 + (1 - cos45) * cornerRadius</code> on top and bottom. 46 * <p> 47 * Since padding is used to offset content for shadows, you cannot set padding on CardView. 48 * Instead, you can use content padding attributes in XML or 49 * {@link #setContentPadding(int, int, int, int)} in code to set the padding between the edges of 50 * the CardView and children of CardView. 51 * <p> 52 * Note that, if you specify exact dimensions for the CardView, because of the shadows, its content 53 * area will be different between platforms before Lollipop and after Lollipop. By using api version 54 * specific resource values, you can avoid these changes. Alternatively, If you want CardView to add 55 * inner padding on platforms Lollipop and after as well, you can call 56 * {@link #setUseCompatPadding(boolean)} and pass <code>true</code>. 57 * <p> 58 * To change CardView's elevation in a backward compatible way, use 59 * {@link #setCardElevation(float)}. CardView will use elevation API on Lollipop and before 60 * Lollipop, it will change the shadow size. To avoid moving the View while shadow size is changing, 61 * shadow size is clamped by {@link #getMaxCardElevation()}. If you want to change elevation 62 * dynamically, you should call {@link #setMaxCardElevation(float)} when CardView is initialized. 63 * 64 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardBackgroundColor 65 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardCornerRadius 66 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardElevation 67 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardMaxElevation 68 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardUseCompatPadding 69 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardPreventCornerOverlap 70 * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPadding 71 * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingLeft 72 * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingTop 73 * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingRight 74 * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingBottom 75 */ 76 public class CardView extends FrameLayout { 77 78 private static final int[] COLOR_BACKGROUND_ATTR = {android.R.attr.colorBackground}; 79 private static final CardViewImpl IMPL; 80 81 static { 82 if (Build.VERSION.SDK_INT >= 21) { 83 IMPL = new CardViewApi21Impl(); 84 } else if (Build.VERSION.SDK_INT >= 17) { 85 IMPL = new CardViewApi17Impl(); 86 } else { 87 IMPL = new CardViewBaseImpl(); 88 } IMPL.initStatic()89 IMPL.initStatic(); 90 } 91 92 private boolean mCompatPadding; 93 94 private boolean mPreventCornerOverlap; 95 96 /** 97 * CardView requires to have a particular minimum size to draw shadows before API 21. If 98 * developer also sets min width/height, they might be overridden. 99 * 100 * CardView works around this issue by recording user given parameters and using an internal 101 * method to set them. 102 */ 103 int mUserSetMinWidth, mUserSetMinHeight; 104 105 final Rect mContentPadding = new Rect(); 106 107 final Rect mShadowBounds = new Rect(); 108 CardView(Context context)109 public CardView(Context context) { 110 super(context); 111 initialize(context, null, 0); 112 } 113 CardView(Context context, AttributeSet attrs)114 public CardView(Context context, AttributeSet attrs) { 115 super(context, attrs); 116 initialize(context, attrs, 0); 117 } 118 CardView(Context context, AttributeSet attrs, int defStyleAttr)119 public CardView(Context context, AttributeSet attrs, int defStyleAttr) { 120 super(context, attrs, defStyleAttr); 121 initialize(context, attrs, defStyleAttr); 122 } 123 124 @Override setPadding(int left, int top, int right, int bottom)125 public void setPadding(int left, int top, int right, int bottom) { 126 // NO OP 127 } 128 129 @Override setPaddingRelative(int start, int top, int end, int bottom)130 public void setPaddingRelative(int start, int top, int end, int bottom) { 131 // NO OP 132 } 133 134 /** 135 * Returns whether CardView will add inner padding on platforms Lollipop and after. 136 * 137 * @return <code>true</code> if CardView adds inner padding on platforms Lollipop and after to 138 * have same dimensions with platforms before Lollipop. 139 */ getUseCompatPadding()140 public boolean getUseCompatPadding() { 141 return mCompatPadding; 142 } 143 144 /** 145 * CardView adds additional padding to draw shadows on platforms before Lollipop. 146 * <p> 147 * This may cause Cards to have different sizes between Lollipop and before Lollipop. If you 148 * need to align CardView with other Views, you may need api version specific dimension 149 * resources to account for the changes. 150 * As an alternative, you can set this flag to <code>true</code> and CardView will add the same 151 * padding values on platforms Lollipop and after. 152 * <p> 153 * Since setting this flag to true adds unnecessary gaps in the UI, default value is 154 * <code>false</code>. 155 * 156 * @param useCompatPadding <code>true></code> if CardView should add padding for the shadows on 157 * platforms Lollipop and above. 158 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardUseCompatPadding 159 */ setUseCompatPadding(boolean useCompatPadding)160 public void setUseCompatPadding(boolean useCompatPadding) { 161 if (mCompatPadding != useCompatPadding) { 162 mCompatPadding = useCompatPadding; 163 IMPL.onCompatPaddingChanged(mCardViewDelegate); 164 } 165 } 166 167 /** 168 * Sets the padding between the Card's edges and the children of CardView. 169 * <p> 170 * Depending on platform version or {@link #getUseCompatPadding()} settings, CardView may 171 * update these values before calling {@link android.view.View#setPadding(int, int, int, int)}. 172 * 173 * @param left The left padding in pixels 174 * @param top The top padding in pixels 175 * @param right The right padding in pixels 176 * @param bottom The bottom padding in pixels 177 * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPadding 178 * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingLeft 179 * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingTop 180 * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingRight 181 * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingBottom 182 */ setContentPadding(int left, int top, int right, int bottom)183 public void setContentPadding(int left, int top, int right, int bottom) { 184 mContentPadding.set(left, top, right, bottom); 185 IMPL.updatePadding(mCardViewDelegate); 186 } 187 188 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)189 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 190 if (!(IMPL instanceof CardViewApi21Impl)) { 191 final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 192 switch (widthMode) { 193 case MeasureSpec.EXACTLY: 194 case MeasureSpec.AT_MOST: 195 final int minWidth = (int) Math.ceil(IMPL.getMinWidth(mCardViewDelegate)); 196 widthMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(minWidth, 197 MeasureSpec.getSize(widthMeasureSpec)), widthMode); 198 break; 199 } 200 201 final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 202 switch (heightMode) { 203 case MeasureSpec.EXACTLY: 204 case MeasureSpec.AT_MOST: 205 final int minHeight = (int) Math.ceil(IMPL.getMinHeight(mCardViewDelegate)); 206 heightMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(minHeight, 207 MeasureSpec.getSize(heightMeasureSpec)), heightMode); 208 break; 209 } 210 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 211 } else { 212 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 213 } 214 } 215 initialize(Context context, AttributeSet attrs, int defStyleAttr)216 private void initialize(Context context, AttributeSet attrs, int defStyleAttr) { 217 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CardView, defStyleAttr, 218 R.style.CardView); 219 ColorStateList backgroundColor; 220 if (a.hasValue(R.styleable.CardView_cardBackgroundColor)) { 221 backgroundColor = a.getColorStateList(R.styleable.CardView_cardBackgroundColor); 222 } else { 223 // There isn't one set, so we'll compute one based on the theme 224 final TypedArray aa = getContext().obtainStyledAttributes(COLOR_BACKGROUND_ATTR); 225 final int themeColorBackground = aa.getColor(0, 0); 226 aa.recycle(); 227 228 // If the theme colorBackground is light, use our own light color, otherwise dark 229 final float[] hsv = new float[3]; 230 Color.colorToHSV(themeColorBackground, hsv); 231 backgroundColor = ColorStateList.valueOf(hsv[2] > 0.5f 232 ? getResources().getColor(R.color.cardview_light_background) 233 : getResources().getColor(R.color.cardview_dark_background)); 234 } 235 float radius = a.getDimension(R.styleable.CardView_cardCornerRadius, 0); 236 float elevation = a.getDimension(R.styleable.CardView_cardElevation, 0); 237 float maxElevation = a.getDimension(R.styleable.CardView_cardMaxElevation, 0); 238 mCompatPadding = a.getBoolean(R.styleable.CardView_cardUseCompatPadding, false); 239 mPreventCornerOverlap = a.getBoolean(R.styleable.CardView_cardPreventCornerOverlap, true); 240 int defaultPadding = a.getDimensionPixelSize(R.styleable.CardView_contentPadding, 0); 241 mContentPadding.left = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingLeft, 242 defaultPadding); 243 mContentPadding.top = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingTop, 244 defaultPadding); 245 mContentPadding.right = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingRight, 246 defaultPadding); 247 mContentPadding.bottom = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingBottom, 248 defaultPadding); 249 if (elevation > maxElevation) { 250 maxElevation = elevation; 251 } 252 mUserSetMinWidth = a.getDimensionPixelSize(R.styleable.CardView_android_minWidth, 0); 253 mUserSetMinHeight = a.getDimensionPixelSize(R.styleable.CardView_android_minHeight, 0); 254 a.recycle(); 255 256 IMPL.initialize(mCardViewDelegate, context, backgroundColor, radius, 257 elevation, maxElevation); 258 } 259 260 @Override setMinimumWidth(int minWidth)261 public void setMinimumWidth(int minWidth) { 262 mUserSetMinWidth = minWidth; 263 super.setMinimumWidth(minWidth); 264 } 265 266 @Override setMinimumHeight(int minHeight)267 public void setMinimumHeight(int minHeight) { 268 mUserSetMinHeight = minHeight; 269 super.setMinimumHeight(minHeight); 270 } 271 272 /** 273 * Updates the background color of the CardView 274 * 275 * @param color The new color to set for the card background 276 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardBackgroundColor 277 */ setCardBackgroundColor(@olorInt int color)278 public void setCardBackgroundColor(@ColorInt int color) { 279 IMPL.setBackgroundColor(mCardViewDelegate, ColorStateList.valueOf(color)); 280 } 281 282 /** 283 * Updates the background ColorStateList of the CardView 284 * 285 * @param color The new ColorStateList to set for the card background 286 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardBackgroundColor 287 */ setCardBackgroundColor(@ullable ColorStateList color)288 public void setCardBackgroundColor(@Nullable ColorStateList color) { 289 IMPL.setBackgroundColor(mCardViewDelegate, color); 290 } 291 292 /** 293 * Returns the background color state list of the CardView. 294 * 295 * @return The background color state list of the CardView. 296 */ getCardBackgroundColor()297 public ColorStateList getCardBackgroundColor() { 298 return IMPL.getBackgroundColor(mCardViewDelegate); 299 } 300 301 /** 302 * Returns the inner padding after the Card's left edge 303 * 304 * @return the inner padding after the Card's left edge 305 */ getContentPaddingLeft()306 public int getContentPaddingLeft() { 307 return mContentPadding.left; 308 } 309 310 /** 311 * Returns the inner padding before the Card's right edge 312 * 313 * @return the inner padding before the Card's right edge 314 */ getContentPaddingRight()315 public int getContentPaddingRight() { 316 return mContentPadding.right; 317 } 318 319 /** 320 * Returns the inner padding after the Card's top edge 321 * 322 * @return the inner padding after the Card's top edge 323 */ getContentPaddingTop()324 public int getContentPaddingTop() { 325 return mContentPadding.top; 326 } 327 328 /** 329 * Returns the inner padding before the Card's bottom edge 330 * 331 * @return the inner padding before the Card's bottom edge 332 */ getContentPaddingBottom()333 public int getContentPaddingBottom() { 334 return mContentPadding.bottom; 335 } 336 337 /** 338 * Updates the corner radius of the CardView. 339 * 340 * @param radius The radius in pixels of the corners of the rectangle shape 341 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardCornerRadius 342 * @see #setRadius(float) 343 */ setRadius(float radius)344 public void setRadius(float radius) { 345 IMPL.setRadius(mCardViewDelegate, radius); 346 } 347 348 /** 349 * Returns the corner radius of the CardView. 350 * 351 * @return Corner radius of the CardView 352 * @see #getRadius() 353 */ getRadius()354 public float getRadius() { 355 return IMPL.getRadius(mCardViewDelegate); 356 } 357 358 /** 359 * Updates the backward compatible elevation of the CardView. 360 * 361 * @param elevation The backward compatible elevation in pixels. 362 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardElevation 363 * @see #getCardElevation() 364 * @see #setMaxCardElevation(float) 365 */ setCardElevation(float elevation)366 public void setCardElevation(float elevation) { 367 IMPL.setElevation(mCardViewDelegate, elevation); 368 } 369 370 /** 371 * Returns the backward compatible elevation of the CardView. 372 * 373 * @return Elevation of the CardView 374 * @see #setCardElevation(float) 375 * @see #getMaxCardElevation() 376 */ getCardElevation()377 public float getCardElevation() { 378 return IMPL.getElevation(mCardViewDelegate); 379 } 380 381 /** 382 * Updates the backward compatible maximum elevation of the CardView. 383 * <p> 384 * Calling this method has no effect if device OS version is Lollipop or newer and 385 * {@link #getUseCompatPadding()} is <code>false</code>. 386 * 387 * @param maxElevation The backward compatible maximum elevation in pixels. 388 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardMaxElevation 389 * @see #setCardElevation(float) 390 * @see #getMaxCardElevation() 391 */ setMaxCardElevation(float maxElevation)392 public void setMaxCardElevation(float maxElevation) { 393 IMPL.setMaxElevation(mCardViewDelegate, maxElevation); 394 } 395 396 /** 397 * Returns the backward compatible maximum elevation of the CardView. 398 * 399 * @return Maximum elevation of the CardView 400 * @see #setMaxCardElevation(float) 401 * @see #getCardElevation() 402 */ getMaxCardElevation()403 public float getMaxCardElevation() { 404 return IMPL.getMaxElevation(mCardViewDelegate); 405 } 406 407 /** 408 * Returns whether CardView should add extra padding to content to avoid overlaps with rounded 409 * corners on pre-Lollipop platforms. 410 * 411 * @return True if CardView prevents overlaps with rounded corners on platforms before Lollipop. 412 * Default value is <code>true</code>. 413 */ getPreventCornerOverlap()414 public boolean getPreventCornerOverlap() { 415 return mPreventCornerOverlap; 416 } 417 418 /** 419 * On pre-Lollipop platforms, CardView does not clip the bounds of the Card for the rounded 420 * corners. Instead, it adds padding to content so that it won't overlap with the rounded 421 * corners. You can disable this behavior by setting this field to <code>false</code>. 422 * <p> 423 * Setting this value on Lollipop and above does not have any effect unless you have enabled 424 * compatibility padding. 425 * 426 * @param preventCornerOverlap Whether CardView should add extra padding to content to avoid 427 * overlaps with the CardView corners. 428 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardPreventCornerOverlap 429 * @see #setUseCompatPadding(boolean) 430 */ setPreventCornerOverlap(boolean preventCornerOverlap)431 public void setPreventCornerOverlap(boolean preventCornerOverlap) { 432 if (preventCornerOverlap != mPreventCornerOverlap) { 433 mPreventCornerOverlap = preventCornerOverlap; 434 IMPL.onPreventCornerOverlapChanged(mCardViewDelegate); 435 } 436 } 437 438 private final CardViewDelegate mCardViewDelegate = new CardViewDelegate() { 439 private Drawable mCardBackground; 440 441 @Override 442 public void setCardBackground(Drawable drawable) { 443 mCardBackground = drawable; 444 setBackgroundDrawable(drawable); 445 } 446 447 @Override 448 public boolean getUseCompatPadding() { 449 return CardView.this.getUseCompatPadding(); 450 } 451 452 @Override 453 public boolean getPreventCornerOverlap() { 454 return CardView.this.getPreventCornerOverlap(); 455 } 456 457 @Override 458 public void setShadowPadding(int left, int top, int right, int bottom) { 459 mShadowBounds.set(left, top, right, bottom); 460 CardView.super.setPadding(left + mContentPadding.left, top + mContentPadding.top, 461 right + mContentPadding.right, bottom + mContentPadding.bottom); 462 } 463 464 @Override 465 public void setMinWidthHeightInternal(int width, int height) { 466 if (width > mUserSetMinWidth) { 467 CardView.super.setMinimumWidth(width); 468 } 469 if (height > mUserSetMinHeight) { 470 CardView.super.setMinimumHeight(height); 471 } 472 } 473 474 @Override 475 public Drawable getCardBackground() { 476 return mCardBackground; 477 } 478 479 @Override 480 public View getCardView() { 481 return CardView.this; 482 } 483 }; 484 } 485