1 /* 2 * Copyright (C) 2014 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.graphics.Canvas; 18 import android.graphics.Color; 19 import android.graphics.Paint; 20 import android.graphics.Rect; 21 import android.util.AttributeSet; 22 import android.view.View; 23 import android.view.ViewGroup; 24 import android.widget.FrameLayout; 25 26 import androidx.annotation.ColorInt; 27 import androidx.leanback.R; 28 29 /** 30 * Provides an SDK version-independent wrapper to support shadows, color overlays, and rounded 31 * corners. It's not always preferred to create a ShadowOverlayContainer, use 32 * {@link ShadowOverlayHelper} instead. 33 * <p> 34 * {@link #prepareParentForShadow(ViewGroup)} must be called on parent of container 35 * before using shadow. Depending on sdk version, optical bounds might be applied 36 * to parent. 37 * </p> 38 * <p> 39 * If shadows can appear outside the bounds of the parent view, setClipChildren(false) must 40 * be called on the grandparent view. 41 * </p> 42 * <p> 43 * {@link #initialize(boolean, boolean, boolean)} must be first called on the container. 44 * Then call {@link #wrap(View)} to insert the wrapped view into the container. 45 * </p> 46 * <p> 47 * Call {@link #setShadowFocusLevel(float)} to control the strength of the shadow (focused shadows 48 * cast stronger shadows). 49 * </p> 50 * <p> 51 * Call {@link #setOverlayColor(int)} to control overlay color. 52 * </p> 53 */ 54 public class ShadowOverlayContainer extends FrameLayout { 55 56 /** 57 * No shadow. 58 */ 59 public static final int SHADOW_NONE = ShadowOverlayHelper.SHADOW_NONE; 60 61 /** 62 * Shadows are fixed. 63 */ 64 public static final int SHADOW_STATIC = ShadowOverlayHelper.SHADOW_STATIC; 65 66 /** 67 * Shadows depend on the size, shape, and position of the view. 68 */ 69 public static final int SHADOW_DYNAMIC = ShadowOverlayHelper.SHADOW_DYNAMIC; 70 71 private boolean mInitialized; 72 private Object mShadowImpl; 73 private View mWrappedView; 74 private boolean mRoundedCorners; 75 private int mShadowType = SHADOW_NONE; 76 private float mUnfocusedZ; 77 private float mFocusedZ; 78 private int mRoundedCornerRadius; 79 private static final Rect sTempRect = new Rect(); 80 private Paint mOverlayPaint; 81 int mOverlayColor; 82 83 /** 84 * Create ShadowOverlayContainer and auto select shadow type. 85 */ 86 public ShadowOverlayContainer(Context context) { 87 this(context, null, 0); 88 } 89 90 /** 91 * Create ShadowOverlayContainer and auto select shadow type. 92 */ 93 public ShadowOverlayContainer(Context context, AttributeSet attrs) { 94 this(context, attrs, 0); 95 } 96 97 /** 98 * Create ShadowOverlayContainer and auto select shadow type. 99 */ 100 public ShadowOverlayContainer(Context context, AttributeSet attrs, int defStyle) { 101 super(context, attrs, defStyle); 102 useStaticShadow(); 103 useDynamicShadow(); 104 } 105 106 /** 107 * Create ShadowOverlayContainer with specific shadowType. 108 */ 109 ShadowOverlayContainer(Context context, 110 int shadowType, boolean hasColorDimOverlay, 111 float unfocusedZ, float focusedZ, int roundedCornerRadius) { 112 super(context); 113 mUnfocusedZ = unfocusedZ; 114 mFocusedZ = focusedZ; 115 initialize(shadowType, hasColorDimOverlay, roundedCornerRadius); 116 } 117 118 /** 119 * Return true if the platform sdk supports shadow. 120 */ 121 public static boolean supportsShadow() { 122 return StaticShadowHelper.supportsShadow(); 123 } 124 125 /** 126 * Returns true if the platform sdk supports dynamic shadows. 127 */ 128 public static boolean supportsDynamicShadow() { 129 return ShadowHelper.supportsDynamicShadow(); 130 } 131 132 /** 133 * {@link #prepareParentForShadow(ViewGroup)} must be called on parent of container 134 * before using shadow. Depending on sdk version, optical bounds might be applied 135 * to parent. 136 */ 137 public static void prepareParentForShadow(ViewGroup parent) { 138 StaticShadowHelper.prepareParent(parent); 139 } 140 141 /** 142 * Sets the shadow type to {@link #SHADOW_DYNAMIC} if supported. 143 */ 144 public void useDynamicShadow() { 145 useDynamicShadow(getResources().getDimension(R.dimen.lb_material_shadow_normal_z), 146 getResources().getDimension(R.dimen.lb_material_shadow_focused_z)); 147 } 148 149 /** 150 * Sets the shadow type to {@link #SHADOW_DYNAMIC} if supported and sets the elevation/Z 151 * values to the given parameters. 152 */ 153 public void useDynamicShadow(float unfocusedZ, float focusedZ) { 154 if (mInitialized) { 155 throw new IllegalStateException("Already initialized"); 156 } 157 if (supportsDynamicShadow()) { 158 mShadowType = SHADOW_DYNAMIC; 159 mUnfocusedZ = unfocusedZ; 160 mFocusedZ = focusedZ; 161 } 162 } 163 164 /** 165 * Sets the shadow type to {@link #SHADOW_STATIC} if supported. 166 */ 167 public void useStaticShadow() { 168 if (mInitialized) { 169 throw new IllegalStateException("Already initialized"); 170 } 171 if (supportsShadow()) { 172 mShadowType = SHADOW_STATIC; 173 } 174 } 175 176 /** 177 * Returns the shadow type, one of {@link #SHADOW_NONE}, {@link #SHADOW_STATIC}, or 178 * {@link #SHADOW_DYNAMIC}. 179 */ 180 public int getShadowType() { 181 return mShadowType; 182 } 183 184 /** 185 * Initialize shadows, color overlay. 186 * @deprecated use {@link ShadowOverlayHelper#createShadowOverlayContainer(Context)} instead. 187 */ 188 @Deprecated 189 public void initialize(boolean hasShadow, boolean hasColorDimOverlay) { 190 initialize(hasShadow, hasColorDimOverlay, true); 191 } 192 193 /** 194 * Initialize shadows, color overlay, and rounded corners. All are optional. 195 * Shadow type are auto-selected based on {@link #useStaticShadow()} and 196 * {@link #useDynamicShadow()} call. 197 * @deprecated use {@link ShadowOverlayHelper#createShadowOverlayContainer(Context)} instead. 198 */ 199 @Deprecated 200 public void initialize(boolean hasShadow, boolean hasColorDimOverlay, boolean roundedCorners) { 201 int shadowType; 202 if (!hasShadow) { 203 shadowType = SHADOW_NONE; 204 } else { 205 shadowType = mShadowType; 206 } 207 int roundedCornerRadius = roundedCorners ? getContext().getResources().getDimensionPixelSize( 208 R.dimen.lb_rounded_rect_corner_radius) : 0; 209 initialize(shadowType, hasColorDimOverlay, roundedCornerRadius); 210 } 211 212 /** 213 * Initialize shadows, color overlay, and rounded corners. All are optional. 214 */ 215 void initialize(int shadowType, boolean hasColorDimOverlay, int roundedCornerRadius) { 216 if (mInitialized) { 217 throw new IllegalStateException(); 218 } 219 mInitialized = true; 220 mRoundedCornerRadius = roundedCornerRadius; 221 mRoundedCorners = roundedCornerRadius > 0; 222 mShadowType = shadowType; 223 switch (mShadowType) { 224 case SHADOW_DYNAMIC: 225 mShadowImpl = ShadowHelper.addDynamicShadow( 226 this, mUnfocusedZ, mFocusedZ, mRoundedCornerRadius); 227 break; 228 case SHADOW_STATIC: 229 mShadowImpl = StaticShadowHelper.addStaticShadow(this); 230 break; 231 } 232 if (hasColorDimOverlay) { 233 setWillNotDraw(false); 234 mOverlayColor = Color.TRANSPARENT; 235 mOverlayPaint = new Paint(); 236 mOverlayPaint.setColor(mOverlayColor); 237 mOverlayPaint.setStyle(Paint.Style.FILL); 238 } else { 239 setWillNotDraw(true); 240 mOverlayPaint = null; 241 } 242 } 243 244 @Override 245 public void draw(Canvas canvas) { 246 super.draw(canvas); 247 if (mOverlayPaint != null && mOverlayColor != Color.TRANSPARENT) { 248 canvas.drawRect(mWrappedView.getLeft(), mWrappedView.getTop(), 249 mWrappedView.getRight(), mWrappedView.getBottom(), 250 mOverlayPaint); 251 } 252 } 253 254 /** 255 * Set shadow focus level (0 to 1). 0 for unfocused, 1f for fully focused. 256 */ 257 public void setShadowFocusLevel(float level) { 258 if (mShadowImpl != null) { 259 ShadowOverlayHelper.setShadowFocusLevel(mShadowImpl, mShadowType, level); 260 } 261 } 262 263 /** 264 * Set color (with alpha) of the overlay. 265 */ 266 public void setOverlayColor(@ColorInt int overlayColor) { 267 if (mOverlayPaint != null) { 268 if (overlayColor != mOverlayColor) { 269 mOverlayColor = overlayColor; 270 mOverlayPaint.setColor(overlayColor); 271 invalidate(); 272 } 273 } 274 } 275 276 /** 277 * Inserts view into the wrapper. 278 */ 279 public void wrap(View view) { 280 if (!mInitialized || mWrappedView != null) { 281 throw new IllegalStateException(); 282 } 283 ViewGroup.LayoutParams lp = view.getLayoutParams(); 284 if (lp != null) { 285 // if wrapped view has layout params, inherit everything but width/height. 286 // Wrapped view is assigned a FrameLayout.LayoutParams with width and height only. 287 // Margins, etc are assigned to the wrapper and take effect in parent container. 288 ViewGroup.LayoutParams wrapped_lp = new FrameLayout.LayoutParams(lp.width, lp.height); 289 // Uses MATCH_PARENT for MATCH_PARENT, WRAP_CONTENT for WRAP_CONTENT and fixed size, 290 // App can still change wrapped view fixed width/height afterwards. 291 lp.width = lp.width == LayoutParams.MATCH_PARENT 292 ? LayoutParams.MATCH_PARENT : LayoutParams.WRAP_CONTENT; 293 lp.height = lp.height == LayoutParams.MATCH_PARENT 294 ? LayoutParams.MATCH_PARENT : LayoutParams.WRAP_CONTENT; 295 this.setLayoutParams(lp); 296 addView(view, wrapped_lp); 297 } else { 298 addView(view); 299 } 300 if (mRoundedCorners && mShadowType != SHADOW_DYNAMIC) { 301 RoundedRectHelper.setClipToRoundedOutline(this, true); 302 } 303 mWrappedView = view; 304 } 305 306 /** 307 * Returns the wrapper view. 308 */ 309 public View getWrappedView() { 310 return mWrappedView; 311 } 312 313 @Override 314 protected void onLayout(boolean changed, int l, int t, int r, int b) { 315 super.onLayout(changed, l, t, r, b); 316 if (changed && mWrappedView != null) { 317 sTempRect.left = (int) mWrappedView.getPivotX(); 318 sTempRect.top = (int) mWrappedView.getPivotY(); 319 offsetDescendantRectToMyCoords(mWrappedView, sTempRect); 320 setPivotX(sTempRect.left); 321 setPivotY(sTempRect.top); 322 } 323 } 324 325 @Override 326 public boolean hasOverlappingRendering() { 327 return false; 328 } 329 } 330