1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14 package androidx.leanback.widget; 15 16 import android.content.Context; 17 import android.content.res.Resources; 18 import android.graphics.drawable.ColorDrawable; 19 import android.graphics.drawable.Drawable; 20 import android.view.View; 21 import android.view.ViewGroup; 22 23 import androidx.leanback.R; 24 import androidx.leanback.system.Settings; 25 26 27 /** 28 * ShadowOverlayHelper is a helper class for shadow, overlay color and rounded corner. 29 * There are many choices to implement Shadow, overlay color. 30 * Initialize it with ShadowOverlayHelper.Builder and it decides the best strategy based 31 * on options user choose and current platform version. 32 * 33 * <li> For shadow: it may use 9-patch with opticalBounds or Z-value based shadow for 34 * API >= 21. When 9-patch is used, it requires a ShadowOverlayContainer 35 * to include 9-patch views. 36 * <li> For overlay: it may use ShadowOverlayContainer which overrides draw() or it may 37 * use setForeground(new ColorDrawable()) for API>=23. The foreground support 38 * might be disabled if rounded corner is applied due to performance reason. 39 * <li> For rounded-corner: it uses a ViewOutlineProvider for API>=21. 40 * 41 * There are two different strategies: use Wrapper with a ShadowOverlayContainer; 42 * or apply rounded corner, overlay and rounded-corner to the view itself. Below is an example 43 * of how helper is used. 44 * 45 * <code> 46 * ShadowOverlayHelper mHelper = new ShadowOverlayHelper.Builder(). 47 * .needsOverlay(true).needsRoundedCorner(true).needsShadow(true) 48 * .build(); 49 * mHelper.prepareParentForShadow(parentView); // apply optical-bounds for 9-patch shadow. 50 * mHelper.setOverlayColor(view, Color.argb(0x80, 0x80, 0x80, 0x80)); 51 * mHelper.setShadowFocusLevel(view, 1.0f); 52 * ... 53 * View initializeView(View view) { 54 * if (mHelper.needsWrapper()) { 55 * ShadowOverlayContainer wrapper = mHelper.createShadowOverlayContainer(context); 56 * wrapper.wrap(view); 57 * return wrapper; 58 * } else { 59 * mHelper.onViewCreated(view); 60 * return view; 61 * } 62 * } 63 * ... 64 * 65 * </code> 66 */ 67 public final class ShadowOverlayHelper { 68 69 /** 70 * Builder for creating ShadowOverlayHelper. 71 */ 72 public static final class Builder { 73 74 private boolean needsOverlay; 75 private boolean needsRoundedCorner; 76 private boolean needsShadow; 77 private boolean preferZOrder = true; 78 private boolean keepForegroundDrawable; 79 private Options options = Options.DEFAULT; 80 81 /** 82 * Set if needs overlay color. 83 * @param needsOverlay True if needs overlay. 84 * @return The Builder object itself. 85 */ needsOverlay(boolean needsOverlay)86 public Builder needsOverlay(boolean needsOverlay) { 87 this.needsOverlay = needsOverlay; 88 return this; 89 } 90 91 /** 92 * Set if needs shadow. 93 * @param needsShadow True if needs shadow. 94 * @return The Builder object itself. 95 */ needsShadow(boolean needsShadow)96 public Builder needsShadow(boolean needsShadow) { 97 this.needsShadow = needsShadow; 98 return this; 99 } 100 101 /** 102 * Set if needs rounded corner. 103 * @param needsRoundedCorner True if needs rounded corner. 104 * @return The Builder object itself. 105 */ needsRoundedCorner(boolean needsRoundedCorner)106 public Builder needsRoundedCorner(boolean needsRoundedCorner) { 107 this.needsRoundedCorner = needsRoundedCorner; 108 return this; 109 } 110 111 /** 112 * Set if prefer z-order shadow. On old devices, z-order shadow might be slow, 113 * set to false to fall back to static 9-patch shadow. Recommend to read 114 * from system wide Setting value: see {@link Settings}. 115 * 116 * @param preferZOrder True if prefer Z shadow. Default is true. 117 * @return The Builder object itself. 118 */ preferZOrder(boolean preferZOrder)119 public Builder preferZOrder(boolean preferZOrder) { 120 this.preferZOrder = preferZOrder; 121 return this; 122 } 123 124 /** 125 * Set if not using foreground drawable for overlay color. For example if 126 * the view has already assigned a foreground drawable for other purposes. 127 * When it's true, helper will use a ShadowOverlayContainer for overlay color. 128 * 129 * @param keepForegroundDrawable True to keep the original foreground drawable. 130 * @return The Builder object itself. 131 */ keepForegroundDrawable(boolean keepForegroundDrawable)132 public Builder keepForegroundDrawable(boolean keepForegroundDrawable) { 133 this.keepForegroundDrawable = keepForegroundDrawable; 134 return this; 135 } 136 137 /** 138 * Set option values e.g. Shadow Z value, rounded corner radius. 139 * 140 * @param options The Options object to create ShadowOverlayHelper. 141 */ options(Options options)142 public Builder options(Options options) { 143 this.options = options; 144 return this; 145 } 146 147 /** 148 * Create ShadowOverlayHelper object 149 * @param context The context uses to read Resources settings. 150 * @return The ShadowOverlayHelper object. 151 */ build(Context context)152 public ShadowOverlayHelper build(Context context) { 153 final ShadowOverlayHelper helper = new ShadowOverlayHelper(); 154 helper.mNeedsOverlay = needsOverlay; 155 helper.mNeedsRoundedCorner = needsRoundedCorner && supportsRoundedCorner(); 156 helper.mNeedsShadow = needsShadow && supportsShadow(); 157 158 if (helper.mNeedsRoundedCorner) { 159 helper.setupRoundedCornerRadius(options, context); 160 } 161 162 // figure out shadow type and if we need use wrapper: 163 if (helper.mNeedsShadow) { 164 // if static shadow is preferred or dynamic shadow is not supported, 165 // use static shadow, otherwise use dynamic shadow. 166 if (!preferZOrder || !supportsDynamicShadow()) { 167 helper.mShadowType = SHADOW_STATIC; 168 // static shadow requires ShadowOverlayContainer to support crossfading 169 // of two shadow views. 170 helper.mNeedsWrapper = true; 171 } else { 172 helper.mShadowType = SHADOW_DYNAMIC; 173 helper.setupDynamicShadowZ(options, context); 174 helper.mNeedsWrapper = ((!supportsForeground() || keepForegroundDrawable) 175 && helper.mNeedsOverlay); 176 } 177 } else { 178 helper.mShadowType = SHADOW_NONE; 179 helper.mNeedsWrapper = ((!supportsForeground() || keepForegroundDrawable) 180 && helper.mNeedsOverlay); 181 } 182 183 return helper; 184 } 185 186 } 187 188 /** 189 * Option values for ShadowOverlayContainer. 190 */ 191 public static final class Options { 192 193 /** 194 * Default Options for values. 195 */ 196 public static final Options DEFAULT = new Options(); 197 198 private int roundedCornerRadius = 0; // 0 for default value 199 private float dynamicShadowUnfocusedZ = -1; // < 0 for default value 200 private float dynamicShadowFocusedZ = -1; // < 0 for default value 201 /** 202 * Set value of rounded corner radius. 203 * 204 * @param roundedCornerRadius Number of pixels of rounded corner radius. 205 * Set to 0 to use default settings. 206 * @return The Options object itself. 207 */ roundedCornerRadius(int roundedCornerRadius)208 public Options roundedCornerRadius(int roundedCornerRadius){ 209 this.roundedCornerRadius = roundedCornerRadius; 210 return this; 211 } 212 213 /** 214 * Set value of focused and unfocused Z value for shadow. 215 * 216 * @param unfocusedZ Number of pixels for unfocused Z value. 217 * @param focusedZ Number of pixels for focused Z value. 218 * @return The Options object itself. 219 */ dynamicShadowZ(float unfocusedZ, float focusedZ)220 public Options dynamicShadowZ(float unfocusedZ, float focusedZ){ 221 this.dynamicShadowUnfocusedZ = unfocusedZ; 222 this.dynamicShadowFocusedZ = focusedZ; 223 return this; 224 } 225 226 /** 227 * Get radius of rounded corner in pixels. 228 * 229 * @return Radius of rounded corner in pixels. 230 */ getRoundedCornerRadius()231 public final int getRoundedCornerRadius() { 232 return roundedCornerRadius; 233 } 234 235 /** 236 * Get z value of shadow when a view is not focused. 237 * 238 * @return Z value of shadow when a view is not focused. 239 */ getDynamicShadowUnfocusedZ()240 public final float getDynamicShadowUnfocusedZ() { 241 return dynamicShadowUnfocusedZ; 242 } 243 244 /** 245 * Get z value of shadow when a view is focused. 246 * 247 * @return Z value of shadow when a view is focused. 248 */ getDynamicShadowFocusedZ()249 public final float getDynamicShadowFocusedZ() { 250 return dynamicShadowFocusedZ; 251 } 252 } 253 254 /** 255 * No shadow. 256 */ 257 public static final int SHADOW_NONE = 1; 258 259 /** 260 * Shadows are fixed. 261 */ 262 public static final int SHADOW_STATIC = 2; 263 264 /** 265 * Shadows depend on the size, shape, and position of the view. 266 */ 267 public static final int SHADOW_DYNAMIC = 3; 268 269 int mShadowType = SHADOW_NONE; 270 boolean mNeedsOverlay; 271 boolean mNeedsRoundedCorner; 272 boolean mNeedsShadow; 273 boolean mNeedsWrapper; 274 275 int mRoundedCornerRadius; 276 float mUnfocusedZ; 277 float mFocusedZ; 278 279 /** 280 * Return true if the platform sdk supports shadow. 281 */ supportsShadow()282 public static boolean supportsShadow() { 283 return StaticShadowHelper.supportsShadow(); 284 } 285 286 /** 287 * Returns true if the platform sdk supports dynamic shadows. 288 */ supportsDynamicShadow()289 public static boolean supportsDynamicShadow() { 290 return ShadowHelper.supportsDynamicShadow(); 291 } 292 293 /** 294 * Returns true if the platform sdk supports rounded corner through outline. 295 */ supportsRoundedCorner()296 public static boolean supportsRoundedCorner() { 297 return RoundedRectHelper.supportsRoundedCorner(); 298 } 299 300 /** 301 * Returns true if view.setForeground() is supported. 302 */ supportsForeground()303 public static boolean supportsForeground() { 304 return ForegroundHelper.supportsForeground(); 305 } 306 307 /* 308 * hide from external, should be only created by ShadowOverlayHelper.Options. 309 */ ShadowOverlayHelper()310 ShadowOverlayHelper() { 311 } 312 313 /** 314 * {@link #prepareParentForShadow(ViewGroup)} must be called on parent of container 315 * before using shadow. Depending on Shadow type, optical bounds might be applied. 316 */ prepareParentForShadow(ViewGroup parent)317 public void prepareParentForShadow(ViewGroup parent) { 318 if (mShadowType == SHADOW_STATIC) { 319 StaticShadowHelper.prepareParent(parent); 320 } 321 } 322 getShadowType()323 public int getShadowType() { 324 return mShadowType; 325 } 326 needsOverlay()327 public boolean needsOverlay() { 328 return mNeedsOverlay; 329 } 330 needsRoundedCorner()331 public boolean needsRoundedCorner() { 332 return mNeedsRoundedCorner; 333 } 334 335 /** 336 * Returns true if a "wrapper" ShadowOverlayContainer is needed. 337 * When needsWrapper() is true, call {@link #createShadowOverlayContainer(Context)} 338 * to create the wrapper. 339 */ needsWrapper()340 public boolean needsWrapper() { 341 return mNeedsWrapper; 342 } 343 344 /** 345 * Create ShadowOverlayContainer for this helper. 346 * @param context Context to create view. 347 * @return ShadowOverlayContainer. 348 */ createShadowOverlayContainer(Context context)349 public ShadowOverlayContainer createShadowOverlayContainer(Context context) { 350 if (!needsWrapper()) { 351 throw new IllegalArgumentException(); 352 } 353 return new ShadowOverlayContainer(context, mShadowType, mNeedsOverlay, 354 mUnfocusedZ, mFocusedZ, mRoundedCornerRadius); 355 } 356 357 /** 358 * Set overlay color for view other than ShadowOverlayContainer. 359 * See also {@link ShadowOverlayContainer#setOverlayColor(int)}. 360 */ setNoneWrapperOverlayColor(View view, int color)361 public static void setNoneWrapperOverlayColor(View view, int color) { 362 Drawable d = ForegroundHelper.getForeground(view); 363 if (d instanceof ColorDrawable) { 364 ((ColorDrawable) d).setColor(color); 365 } else { 366 ForegroundHelper.setForeground(view, new ColorDrawable(color)); 367 } 368 } 369 370 /** 371 * Set overlay color for view, it can be a ShadowOverlayContainer if needsWrapper() is true, 372 * or other view type. 373 */ setOverlayColor(View view, int color)374 public void setOverlayColor(View view, int color) { 375 if (needsWrapper()) { 376 ((ShadowOverlayContainer) view).setOverlayColor(color); 377 } else { 378 setNoneWrapperOverlayColor(view, color); 379 } 380 } 381 382 /** 383 * Must be called when view is created for cases {@link #needsWrapper()} is false. 384 * @param view 385 */ onViewCreated(View view)386 public void onViewCreated(View view) { 387 if (!needsWrapper()) { 388 if (!mNeedsShadow) { 389 if (mNeedsRoundedCorner) { 390 RoundedRectHelper.setClipToRoundedOutline(view, true, mRoundedCornerRadius); 391 } 392 } else { 393 if (mShadowType == SHADOW_DYNAMIC) { 394 Object tag = ShadowHelper.addDynamicShadow( 395 view, mUnfocusedZ, mFocusedZ, mRoundedCornerRadius); 396 view.setTag(R.id.lb_shadow_impl, tag); 397 } else if (mNeedsRoundedCorner) { 398 RoundedRectHelper.setClipToRoundedOutline(view, true, mRoundedCornerRadius); 399 } 400 } 401 } 402 } 403 404 /** 405 * Set shadow focus level (0 to 1). 0 for unfocused, 1 for fully focused. 406 * This is for view other than ShadowOverlayContainer. 407 * See also {@link ShadowOverlayContainer#setShadowFocusLevel(float)}. 408 */ setNoneWrapperShadowFocusLevel(View view, float level)409 public static void setNoneWrapperShadowFocusLevel(View view, float level) { 410 setShadowFocusLevel(getNoneWrapperDynamicShadowImpl(view), SHADOW_DYNAMIC, level); 411 } 412 413 /** 414 * Set shadow focus level (0 to 1). 0 for unfocused, 1 for fully focused. 415 */ setShadowFocusLevel(View view, float level)416 public void setShadowFocusLevel(View view, float level) { 417 if (needsWrapper()) { 418 ((ShadowOverlayContainer) view).setShadowFocusLevel(level); 419 } else { 420 setShadowFocusLevel(getNoneWrapperDynamicShadowImpl(view), SHADOW_DYNAMIC, level); 421 } 422 } 423 setupDynamicShadowZ(Options options, Context context)424 void setupDynamicShadowZ(Options options, Context context) { 425 if (options.getDynamicShadowUnfocusedZ() < 0f) { 426 Resources res = context.getResources(); 427 mFocusedZ = res.getDimension(R.dimen.lb_material_shadow_focused_z); 428 mUnfocusedZ = res.getDimension(R.dimen.lb_material_shadow_normal_z); 429 } else { 430 mFocusedZ = options.getDynamicShadowFocusedZ(); 431 mUnfocusedZ = options.getDynamicShadowUnfocusedZ(); 432 } 433 } 434 setupRoundedCornerRadius(Options options, Context context)435 void setupRoundedCornerRadius(Options options, Context context) { 436 if (options.getRoundedCornerRadius() == 0) { 437 Resources res = context.getResources(); 438 mRoundedCornerRadius = res.getDimensionPixelSize( 439 R.dimen.lb_rounded_rect_corner_radius); 440 } else { 441 mRoundedCornerRadius = options.getRoundedCornerRadius(); 442 } 443 } 444 getNoneWrapperDynamicShadowImpl(View view)445 static Object getNoneWrapperDynamicShadowImpl(View view) { 446 return view.getTag(R.id.lb_shadow_impl); 447 } 448 setShadowFocusLevel(Object impl, int shadowType, float level)449 static void setShadowFocusLevel(Object impl, int shadowType, float level) { 450 if (impl != null) { 451 if (level < 0f) { 452 level = 0f; 453 } else if (level > 1f) { 454 level = 1f; 455 } 456 switch (shadowType) { 457 case SHADOW_DYNAMIC: 458 ShadowHelper.setShadowFocusLevel(impl, level); 459 break; 460 case SHADOW_STATIC: 461 StaticShadowHelper.setShadowFocusLevel(impl, level); 462 break; 463 } 464 } 465 } 466 } 467