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