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 case MeasureSpec.UNSPECIFIED: 200 // Do nothing 201 break; 202 } 203 204 final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 205 switch (heightMode) { 206 case MeasureSpec.EXACTLY: 207 case MeasureSpec.AT_MOST: 208 final int minHeight = (int) Math.ceil(IMPL.getMinHeight(mCardViewDelegate)); 209 heightMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(minHeight, 210 MeasureSpec.getSize(heightMeasureSpec)), heightMode); 211 break; 212 case MeasureSpec.UNSPECIFIED: 213 // Do nothing 214 break; 215 } 216 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 217 } else { 218 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 219 } 220 } 221 initialize(Context context, AttributeSet attrs, int defStyleAttr)222 private void initialize(Context context, AttributeSet attrs, int defStyleAttr) { 223 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CardView, defStyleAttr, 224 R.style.CardView); 225 ColorStateList backgroundColor; 226 if (a.hasValue(R.styleable.CardView_cardBackgroundColor)) { 227 backgroundColor = a.getColorStateList(R.styleable.CardView_cardBackgroundColor); 228 } else { 229 // There isn't one set, so we'll compute one based on the theme 230 final TypedArray aa = getContext().obtainStyledAttributes(COLOR_BACKGROUND_ATTR); 231 final int themeColorBackground = aa.getColor(0, 0); 232 aa.recycle(); 233 234 // If the theme colorBackground is light, use our own light color, otherwise dark 235 final float[] hsv = new float[3]; 236 Color.colorToHSV(themeColorBackground, hsv); 237 backgroundColor = ColorStateList.valueOf(hsv[2] > 0.5f 238 ? getResources().getColor(R.color.cardview_light_background) 239 : getResources().getColor(R.color.cardview_dark_background)); 240 } 241 float radius = a.getDimension(R.styleable.CardView_cardCornerRadius, 0); 242 float elevation = a.getDimension(R.styleable.CardView_cardElevation, 0); 243 float maxElevation = a.getDimension(R.styleable.CardView_cardMaxElevation, 0); 244 mCompatPadding = a.getBoolean(R.styleable.CardView_cardUseCompatPadding, false); 245 mPreventCornerOverlap = a.getBoolean(R.styleable.CardView_cardPreventCornerOverlap, true); 246 int defaultPadding = a.getDimensionPixelSize(R.styleable.CardView_contentPadding, 0); 247 mContentPadding.left = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingLeft, 248 defaultPadding); 249 mContentPadding.top = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingTop, 250 defaultPadding); 251 mContentPadding.right = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingRight, 252 defaultPadding); 253 mContentPadding.bottom = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingBottom, 254 defaultPadding); 255 if (elevation > maxElevation) { 256 maxElevation = elevation; 257 } 258 mUserSetMinWidth = a.getDimensionPixelSize(R.styleable.CardView_android_minWidth, 0); 259 mUserSetMinHeight = a.getDimensionPixelSize(R.styleable.CardView_android_minHeight, 0); 260 a.recycle(); 261 262 IMPL.initialize(mCardViewDelegate, context, backgroundColor, radius, 263 elevation, maxElevation); 264 } 265 266 @Override setMinimumWidth(int minWidth)267 public void setMinimumWidth(int minWidth) { 268 mUserSetMinWidth = minWidth; 269 super.setMinimumWidth(minWidth); 270 } 271 272 @Override setMinimumHeight(int minHeight)273 public void setMinimumHeight(int minHeight) { 274 mUserSetMinHeight = minHeight; 275 super.setMinimumHeight(minHeight); 276 } 277 278 /** 279 * Updates the background color of the CardView 280 * 281 * @param color The new color to set for the card background 282 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardBackgroundColor 283 */ setCardBackgroundColor(@olorInt int color)284 public void setCardBackgroundColor(@ColorInt int color) { 285 IMPL.setBackgroundColor(mCardViewDelegate, ColorStateList.valueOf(color)); 286 } 287 288 /** 289 * Updates the background ColorStateList of the CardView 290 * 291 * @param color The new ColorStateList to set for the card background 292 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardBackgroundColor 293 */ setCardBackgroundColor(@ullable ColorStateList color)294 public void setCardBackgroundColor(@Nullable ColorStateList color) { 295 IMPL.setBackgroundColor(mCardViewDelegate, color); 296 } 297 298 /** 299 * Returns the background color state list of the CardView. 300 * 301 * @return The background color state list of the CardView. 302 */ getCardBackgroundColor()303 public ColorStateList getCardBackgroundColor() { 304 return IMPL.getBackgroundColor(mCardViewDelegate); 305 } 306 307 /** 308 * Returns the inner padding after the Card's left edge 309 * 310 * @return the inner padding after the Card's left edge 311 */ getContentPaddingLeft()312 public int getContentPaddingLeft() { 313 return mContentPadding.left; 314 } 315 316 /** 317 * Returns the inner padding before the Card's right edge 318 * 319 * @return the inner padding before the Card's right edge 320 */ getContentPaddingRight()321 public int getContentPaddingRight() { 322 return mContentPadding.right; 323 } 324 325 /** 326 * Returns the inner padding after the Card's top edge 327 * 328 * @return the inner padding after the Card's top edge 329 */ getContentPaddingTop()330 public int getContentPaddingTop() { 331 return mContentPadding.top; 332 } 333 334 /** 335 * Returns the inner padding before the Card's bottom edge 336 * 337 * @return the inner padding before the Card's bottom edge 338 */ getContentPaddingBottom()339 public int getContentPaddingBottom() { 340 return mContentPadding.bottom; 341 } 342 343 /** 344 * Updates the corner radius of the CardView. 345 * 346 * @param radius The radius in pixels of the corners of the rectangle shape 347 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardCornerRadius 348 * @see #setRadius(float) 349 */ setRadius(float radius)350 public void setRadius(float radius) { 351 IMPL.setRadius(mCardViewDelegate, radius); 352 } 353 354 /** 355 * Returns the corner radius of the CardView. 356 * 357 * @return Corner radius of the CardView 358 * @see #getRadius() 359 */ getRadius()360 public float getRadius() { 361 return IMPL.getRadius(mCardViewDelegate); 362 } 363 364 /** 365 * Updates the backward compatible elevation of the CardView. 366 * 367 * @param elevation The backward compatible elevation in pixels. 368 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardElevation 369 * @see #getCardElevation() 370 * @see #setMaxCardElevation(float) 371 */ setCardElevation(float elevation)372 public void setCardElevation(float elevation) { 373 IMPL.setElevation(mCardViewDelegate, elevation); 374 } 375 376 /** 377 * Returns the backward compatible elevation of the CardView. 378 * 379 * @return Elevation of the CardView 380 * @see #setCardElevation(float) 381 * @see #getMaxCardElevation() 382 */ getCardElevation()383 public float getCardElevation() { 384 return IMPL.getElevation(mCardViewDelegate); 385 } 386 387 /** 388 * Updates the backward compatible maximum elevation of the CardView. 389 * <p> 390 * Calling this method has no effect if device OS version is Lollipop or newer and 391 * {@link #getUseCompatPadding()} is <code>false</code>. 392 * 393 * @param maxElevation The backward compatible maximum elevation in pixels. 394 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardMaxElevation 395 * @see #setCardElevation(float) 396 * @see #getMaxCardElevation() 397 */ setMaxCardElevation(float maxElevation)398 public void setMaxCardElevation(float maxElevation) { 399 IMPL.setMaxElevation(mCardViewDelegate, maxElevation); 400 } 401 402 /** 403 * Returns the backward compatible maximum elevation of the CardView. 404 * 405 * @return Maximum elevation of the CardView 406 * @see #setMaxCardElevation(float) 407 * @see #getCardElevation() 408 */ getMaxCardElevation()409 public float getMaxCardElevation() { 410 return IMPL.getMaxElevation(mCardViewDelegate); 411 } 412 413 /** 414 * Returns whether CardView should add extra padding to content to avoid overlaps with rounded 415 * corners on pre-Lollipop platforms. 416 * 417 * @return True if CardView prevents overlaps with rounded corners on platforms before Lollipop. 418 * Default value is <code>true</code>. 419 */ getPreventCornerOverlap()420 public boolean getPreventCornerOverlap() { 421 return mPreventCornerOverlap; 422 } 423 424 /** 425 * On pre-Lollipop platforms, CardView does not clip the bounds of the Card for the rounded 426 * corners. Instead, it adds padding to content so that it won't overlap with the rounded 427 * corners. You can disable this behavior by setting this field to <code>false</code>. 428 * <p> 429 * Setting this value on Lollipop and above does not have any effect unless you have enabled 430 * compatibility padding. 431 * 432 * @param preventCornerOverlap Whether CardView should add extra padding to content to avoid 433 * overlaps with the CardView corners. 434 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardPreventCornerOverlap 435 * @see #setUseCompatPadding(boolean) 436 */ setPreventCornerOverlap(boolean preventCornerOverlap)437 public void setPreventCornerOverlap(boolean preventCornerOverlap) { 438 if (preventCornerOverlap != mPreventCornerOverlap) { 439 mPreventCornerOverlap = preventCornerOverlap; 440 IMPL.onPreventCornerOverlapChanged(mCardViewDelegate); 441 } 442 } 443 444 private final CardViewDelegate mCardViewDelegate = new CardViewDelegate() { 445 private Drawable mCardBackground; 446 447 @Override 448 public void setCardBackground(Drawable drawable) { 449 mCardBackground = drawable; 450 setBackgroundDrawable(drawable); 451 } 452 453 @Override 454 public boolean getUseCompatPadding() { 455 return CardView.this.getUseCompatPadding(); 456 } 457 458 @Override 459 public boolean getPreventCornerOverlap() { 460 return CardView.this.getPreventCornerOverlap(); 461 } 462 463 @Override 464 public void setShadowPadding(int left, int top, int right, int bottom) { 465 mShadowBounds.set(left, top, right, bottom); 466 CardView.super.setPadding(left + mContentPadding.left, top + mContentPadding.top, 467 right + mContentPadding.right, bottom + mContentPadding.bottom); 468 } 469 470 @Override 471 public void setMinWidthHeightInternal(int width, int height) { 472 if (width > mUserSetMinWidth) { 473 CardView.super.setMinimumWidth(width); 474 } 475 if (height > mUserSetMinHeight) { 476 CardView.super.setMinimumHeight(height); 477 } 478 } 479 480 @Override 481 public Drawable getCardBackground() { 482 return mCardBackground; 483 } 484 485 @Override 486 public View getCardView() { 487 return CardView.this; 488 } 489 }; 490 } 491