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 17package android.support.v7.widget; 18 19import android.content.Context; 20import android.content.res.ColorStateList; 21import android.content.res.TypedArray; 22import android.graphics.Color; 23import android.graphics.Rect; 24import android.graphics.drawable.Drawable; 25import android.os.Build; 26import android.support.annotation.ColorInt; 27import android.support.annotation.Nullable; 28import android.support.v7.cardview.R; 29import android.util.AttributeSet; 30import android.view.View; 31import 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 */ 76public 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 CardViewApi21(); 84 } else if (Build.VERSION.SDK_INT >= 17) { 85 IMPL = new CardViewJellybeanMr1(); 86 } else { 87 IMPL = new CardViewEclairMr1(); 88 } 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 private int mUserSetMinWidth, mUserSetMinHeight; 104 105 private final Rect mContentPadding = new Rect(); 106 107 private final Rect mShadowBounds = new Rect(); 108 109 public CardView(Context context) { 110 super(context); 111 initialize(context, null, 0); 112 } 113 114 public CardView(Context context, AttributeSet attrs) { 115 super(context, attrs); 116 initialize(context, attrs, 0); 117 } 118 119 public CardView(Context context, AttributeSet attrs, int defStyleAttr) { 120 super(context, attrs, defStyleAttr); 121 initialize(context, attrs, defStyleAttr); 122 } 123 124 @Override 125 public void setPadding(int left, int top, int right, int bottom) { 126 // NO OP 127 } 128 129 public void setPaddingRelative(int start, int top, int end, int bottom) { 130 // NO OP 131 } 132 133 /** 134 * Returns whether CardView will add inner padding on platforms Lollipop and after. 135 * 136 * @return <code>true</code> if CardView adds inner padding on platforms Lollipop and after to 137 * have same dimensions with platforms before Lollipop. 138 */ 139 public boolean getUseCompatPadding() { 140 return mCompatPadding; 141 } 142 143 /** 144 * CardView adds additional padding to draw shadows on platforms before Lollipop. 145 * <p> 146 * This may cause Cards to have different sizes between Lollipop and before Lollipop. If you 147 * need to align CardView with other Views, you may need api version specific dimension 148 * resources to account for the changes. 149 * As an alternative, you can set this flag to <code>true</code> and CardView will add the same 150 * padding values on platforms Lollipop and after. 151 * <p> 152 * Since setting this flag to true adds unnecessary gaps in the UI, default value is 153 * <code>false</code>. 154 * 155 * @param useCompatPadding <code>true></code> if CardView should add padding for the shadows on 156 * platforms Lollipop and above. 157 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardUseCompatPadding 158 */ 159 public void setUseCompatPadding(boolean useCompatPadding) { 160 if (mCompatPadding != useCompatPadding) { 161 mCompatPadding = useCompatPadding; 162 IMPL.onCompatPaddingChanged(mCardViewDelegate); 163 } 164 } 165 166 /** 167 * Sets the padding between the Card's edges and the children of CardView. 168 * <p> 169 * Depending on platform version or {@link #getUseCompatPadding()} settings, CardView may 170 * update these values before calling {@link android.view.View#setPadding(int, int, int, int)}. 171 * 172 * @param left The left padding in pixels 173 * @param top The top padding in pixels 174 * @param right The right padding in pixels 175 * @param bottom The bottom padding in pixels 176 * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPadding 177 * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingLeft 178 * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingTop 179 * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingRight 180 * @attr ref android.support.v7.cardview.R.styleable#CardView_contentPaddingBottom 181 */ 182 public void setContentPadding(int left, int top, int right, int bottom) { 183 mContentPadding.set(left, top, right, bottom); 184 IMPL.updatePadding(mCardViewDelegate); 185 } 186 187 @Override 188 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 189 if (!(IMPL instanceof CardViewApi21)) { 190 final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 191 switch (widthMode) { 192 case MeasureSpec.EXACTLY: 193 case MeasureSpec.AT_MOST: 194 final int minWidth = (int) Math.ceil(IMPL.getMinWidth(mCardViewDelegate)); 195 widthMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(minWidth, 196 MeasureSpec.getSize(widthMeasureSpec)), widthMode); 197 break; 198 } 199 200 final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 201 switch (heightMode) { 202 case MeasureSpec.EXACTLY: 203 case MeasureSpec.AT_MOST: 204 final int minHeight = (int) Math.ceil(IMPL.getMinHeight(mCardViewDelegate)); 205 heightMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(minHeight, 206 MeasureSpec.getSize(heightMeasureSpec)), heightMode); 207 break; 208 } 209 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 210 } else { 211 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 212 } 213 } 214 215 private void initialize(Context context, AttributeSet attrs, int defStyleAttr) { 216 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CardView, defStyleAttr, 217 R.style.CardView); 218 ColorStateList backgroundColor; 219 if (a.hasValue(R.styleable.CardView_cardBackgroundColor)) { 220 backgroundColor = a.getColorStateList(R.styleable.CardView_cardBackgroundColor); 221 } else { 222 // There isn't one set, so we'll compute one based on the theme 223 final TypedArray aa = getContext().obtainStyledAttributes(COLOR_BACKGROUND_ATTR); 224 final int themeColorBackground = aa.getColor(0, 0); 225 aa.recycle(); 226 227 // If the theme colorBackground is light, use our own light color, otherwise dark 228 final float[] hsv = new float[3]; 229 Color.colorToHSV(themeColorBackground, hsv); 230 backgroundColor = ColorStateList.valueOf(hsv[2] > 0.5f 231 ? getResources().getColor(R.color.cardview_light_background) 232 : getResources().getColor(R.color.cardview_dark_background)); 233 } 234 float radius = a.getDimension(R.styleable.CardView_cardCornerRadius, 0); 235 float elevation = a.getDimension(R.styleable.CardView_cardElevation, 0); 236 float maxElevation = a.getDimension(R.styleable.CardView_cardMaxElevation, 0); 237 mCompatPadding = a.getBoolean(R.styleable.CardView_cardUseCompatPadding, false); 238 mPreventCornerOverlap = a.getBoolean(R.styleable.CardView_cardPreventCornerOverlap, true); 239 int defaultPadding = a.getDimensionPixelSize(R.styleable.CardView_contentPadding, 0); 240 mContentPadding.left = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingLeft, 241 defaultPadding); 242 mContentPadding.top = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingTop, 243 defaultPadding); 244 mContentPadding.right = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingRight, 245 defaultPadding); 246 mContentPadding.bottom = a.getDimensionPixelSize(R.styleable.CardView_contentPaddingBottom, 247 defaultPadding); 248 if (elevation > maxElevation) { 249 maxElevation = elevation; 250 } 251 mUserSetMinWidth = a.getDimensionPixelSize(R.styleable.CardView_android_minWidth, 0); 252 mUserSetMinHeight = a.getDimensionPixelSize(R.styleable.CardView_android_minHeight, 0); 253 a.recycle(); 254 255 IMPL.initialize(mCardViewDelegate, context, backgroundColor, radius, 256 elevation, maxElevation); 257 } 258 259 @Override 260 public void setMinimumWidth(int minWidth) { 261 mUserSetMinWidth = minWidth; 262 super.setMinimumWidth(minWidth); 263 } 264 265 @Override 266 public void setMinimumHeight(int minHeight) { 267 mUserSetMinHeight = minHeight; 268 super.setMinimumHeight(minHeight); 269 } 270 271 /** 272 * Updates the background color of the CardView 273 * 274 * @param color The new color to set for the card background 275 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardBackgroundColor 276 */ 277 public void setCardBackgroundColor(@ColorInt int color) { 278 IMPL.setBackgroundColor(mCardViewDelegate, ColorStateList.valueOf(color)); 279 } 280 281 /** 282 * Updates the background ColorStateList of the CardView 283 * 284 * @param color The new ColorStateList to set for the card background 285 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardBackgroundColor 286 */ 287 public void setCardBackgroundColor(@Nullable ColorStateList color) { 288 IMPL.setBackgroundColor(mCardViewDelegate, color); 289 } 290 291 /** 292 * Returns the background color state list of the CardView. 293 * 294 * @return The background color state list of the CardView. 295 */ 296 public ColorStateList getCardBackgroundColor() { 297 return IMPL.getBackgroundColor(mCardViewDelegate); 298 } 299 300 /** 301 * Returns the inner padding after the Card's left edge 302 * 303 * @return the inner padding after the Card's left edge 304 */ 305 public int getContentPaddingLeft() { 306 return mContentPadding.left; 307 } 308 309 /** 310 * Returns the inner padding before the Card's right edge 311 * 312 * @return the inner padding before the Card's right edge 313 */ 314 public int getContentPaddingRight() { 315 return mContentPadding.right; 316 } 317 318 /** 319 * Returns the inner padding after the Card's top edge 320 * 321 * @return the inner padding after the Card's top edge 322 */ 323 public int getContentPaddingTop() { 324 return mContentPadding.top; 325 } 326 327 /** 328 * Returns the inner padding before the Card's bottom edge 329 * 330 * @return the inner padding before the Card's bottom edge 331 */ 332 public int getContentPaddingBottom() { 333 return mContentPadding.bottom; 334 } 335 336 /** 337 * Updates the corner radius of the CardView. 338 * 339 * @param radius The radius in pixels of the corners of the rectangle shape 340 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardCornerRadius 341 * @see #setRadius(float) 342 */ 343 public void setRadius(float radius) { 344 IMPL.setRadius(mCardViewDelegate, radius); 345 } 346 347 /** 348 * Returns the corner radius of the CardView. 349 * 350 * @return Corner radius of the CardView 351 * @see #getRadius() 352 */ 353 public float getRadius() { 354 return IMPL.getRadius(mCardViewDelegate); 355 } 356 357 /** 358 * Updates the backward compatible elevation of the CardView. 359 * 360 * @param elevation The backward compatible elevation in pixels. 361 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardElevation 362 * @see #getCardElevation() 363 * @see #setMaxCardElevation(float) 364 */ 365 public void setCardElevation(float elevation) { 366 IMPL.setElevation(mCardViewDelegate, elevation); 367 } 368 369 /** 370 * Returns the backward compatible elevation of the CardView. 371 * 372 * @return Elevation of the CardView 373 * @see #setCardElevation(float) 374 * @see #getMaxCardElevation() 375 */ 376 public float getCardElevation() { 377 return IMPL.getElevation(mCardViewDelegate); 378 } 379 380 /** 381 * Updates the backward compatible maximum elevation of the CardView. 382 * <p> 383 * Calling this method has no effect if device OS version is Lollipop or newer and 384 * {@link #getUseCompatPadding()} is <code>false</code>. 385 * 386 * @param maxElevation The backward compatible maximum elevation in pixels. 387 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardMaxElevation 388 * @see #setCardElevation(float) 389 * @see #getMaxCardElevation() 390 */ 391 public void setMaxCardElevation(float maxElevation) { 392 IMPL.setMaxElevation(mCardViewDelegate, maxElevation); 393 } 394 395 /** 396 * Returns the backward compatible maximum elevation of the CardView. 397 * 398 * @return Maximum elevation of the CardView 399 * @see #setMaxCardElevation(float) 400 * @see #getCardElevation() 401 */ 402 public float getMaxCardElevation() { 403 return IMPL.getMaxElevation(mCardViewDelegate); 404 } 405 406 /** 407 * Returns whether CardView should add extra padding to content to avoid overlaps with rounded 408 * corners on pre-Lollipop platforms. 409 * 410 * @return True if CardView prevents overlaps with rounded corners on platforms before Lollipop. 411 * Default value is <code>true</code>. 412 */ 413 public boolean getPreventCornerOverlap() { 414 return mPreventCornerOverlap; 415 } 416 417 /** 418 * On pre-Lollipop platforms, CardView does not clip the bounds of the Card for the rounded 419 * corners. Instead, it adds padding to content so that it won't overlap with the rounded 420 * corners. You can disable this behavior by setting this field to <code>false</code>. 421 * <p> 422 * Setting this value on Lollipop and above does not have any effect unless you have enabled 423 * compatibility padding. 424 * 425 * @param preventCornerOverlap Whether CardView should add extra padding to content to avoid 426 * overlaps with the CardView corners. 427 * @attr ref android.support.v7.cardview.R.styleable#CardView_cardPreventCornerOverlap 428 * @see #setUseCompatPadding(boolean) 429 */ 430 public void setPreventCornerOverlap(boolean preventCornerOverlap) { 431 if (preventCornerOverlap != mPreventCornerOverlap) { 432 mPreventCornerOverlap = preventCornerOverlap; 433 IMPL.onPreventCornerOverlapChanged(mCardViewDelegate); 434 } 435 } 436 437 private final CardViewDelegate mCardViewDelegate = new CardViewDelegate() { 438 private Drawable mCardBackground; 439 440 @Override 441 public void setCardBackground(Drawable drawable) { 442 mCardBackground = drawable; 443 setBackgroundDrawable(drawable); 444 } 445 446 @Override 447 public boolean getUseCompatPadding() { 448 return CardView.this.getUseCompatPadding(); 449 } 450 451 @Override 452 public boolean getPreventCornerOverlap() { 453 return CardView.this.getPreventCornerOverlap(); 454 } 455 456 @Override 457 public void setShadowPadding(int left, int top, int right, int bottom) { 458 mShadowBounds.set(left, top, right, bottom); 459 CardView.super.setPadding(left + mContentPadding.left, top + mContentPadding.top, 460 right + mContentPadding.right, bottom + mContentPadding.bottom); 461 } 462 463 @Override 464 public void setMinWidthHeightInternal(int width, int height) { 465 if (width > mUserSetMinWidth) { 466 CardView.super.setMinimumWidth(width); 467 } 468 if (height > mUserSetMinHeight) { 469 CardView.super.setMinimumHeight(height); 470 } 471 } 472 473 @Override 474 public Drawable getCardBackground() { 475 return mCardBackground; 476 } 477 478 @Override 479 public View getCardView() { 480 return CardView.this; 481 } 482 }; 483} 484