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