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