1 /* 2 * Copyright (C) 2014 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 com.android.tv.settings.widget; 18 19 import android.content.Context; 20 import android.content.res.TypedArray; 21 import android.graphics.Canvas; 22 import android.graphics.Matrix; 23 import android.graphics.Rect; 24 import android.graphics.RectF; 25 import android.graphics.drawable.Drawable; 26 import android.graphics.drawable.ShapeDrawable; 27 import android.graphics.drawable.shapes.RectShape; 28 import android.util.AttributeSet; 29 import android.view.View; 30 import android.view.ViewDebug.ExportedProperty; 31 import android.view.ViewGroup; 32 import android.view.ViewParent; 33 import android.widget.FrameLayout; 34 import android.widget.ImageView; 35 36 import com.android.tv.settings.R; 37 38 import java.util.ArrayList; 39 40 /** 41 * Allows a drawable to be added for shadowing views in this layout. The shadows 42 * will automatically be sized to wrap their corresponding view. The default 43 * drawable to use can be set in xml by defining the namespace and then using 44 * defaultShadow="@drawable/reference" 45 * <p> 46 * In code views can then have Shadows added to them via 47 * {@link #addShadowView(View)} to use the default drawable or with 48 * {@link #addShadowView(View, Drawable)}. 49 */ 50 public class FrameLayoutWithShadows extends FrameLayout { 51 52 private static final int MAX_RECYCLE = 12; 53 54 static class ShadowView extends View { 55 56 private View shadowedView; 57 private Drawable mDrawableBottom; 58 private float mAlpha = 1f; 59 60 ShadowView(Context context) { 61 super(context); 62 setWillNotDraw(false); 63 } 64 65 void init() { 66 shadowedView = null; 67 mDrawableBottom = null; 68 } 69 70 @Override 71 public void setBackground(Drawable background) { 72 super.setBackground(background); 73 if (background != null) { 74 // framework adds a callback on background to trigger a repaint 75 // when call Drawable.setAlpha(), this is not desired when we override 76 // setAlpha(); if we call Drawable.setAlpha() in the overriden 77 // setAlpha(), it will trigger another repaint event thus cause system 78 // never stop rendering. 79 background.setCallback(null); 80 background.setAlpha((int)(255 * mAlpha)); 81 } 82 } 83 84 @Override 85 public void setAlpha(float alpha) { 86 if (mAlpha != alpha) { 87 mAlpha = alpha; 88 Drawable d = getBackground(); 89 int alphaMulitplied = (int)(alpha * 255); 90 if (d != null) { 91 d.setAlpha(alphaMulitplied); 92 } 93 if (mDrawableBottom != null) { 94 mDrawableBottom.setAlpha(alphaMulitplied); 95 } 96 invalidate(); 97 } 98 } 99 100 @Override 101 @ExportedProperty(category = "drawing") 102 public float getAlpha() { 103 return mAlpha; 104 } 105 106 @Override 107 protected boolean onSetAlpha(int alpha) { 108 return true; 109 } 110 111 public void setDrawableBottom(Drawable drawable) { 112 mDrawableBottom = drawable; 113 if (mAlpha >= 0) { 114 mDrawableBottom.setAlpha((int)(255 * mAlpha)); 115 } 116 invalidate(); 117 } 118 119 @Override 120 protected void onDraw(Canvas canvas) { 121 // draw background 9 patch 122 super.onDraw(canvas); 123 // draw bottom 124 if (mDrawableBottom != null) { 125 mDrawableBottom.setBounds(getPaddingLeft(), getHeight() - getPaddingBottom(), 126 getWidth() - getPaddingRight(), getHeight() - getPaddingBottom() 127 + mDrawableBottom.getIntrinsicHeight()); 128 mDrawableBottom.draw(canvas); 129 } 130 } 131 } 132 133 private final Rect rect = new Rect(); 134 private final RectF rectf = new RectF(); 135 private int mShadowResourceId; 136 private int mBottomResourceId; 137 private float mShadowsAlpha = 1f; 138 private final ArrayList<ShadowView> mRecycleBin = new ArrayList<>(MAX_RECYCLE); 139 140 public FrameLayoutWithShadows(Context context) { 141 this(context, null); 142 } 143 144 public FrameLayoutWithShadows(Context context, AttributeSet attrs) { 145 this(context, attrs, 0); 146 } 147 148 public FrameLayoutWithShadows(Context context, AttributeSet attrs, int defStyle) { 149 super(context, attrs, defStyle); 150 initFromAttributes(context, attrs); 151 } 152 153 @Override 154 protected void onLayout(boolean changed, int l, int t, int r, int b) { 155 super.onLayout(changed, l, t, r, b); 156 layoutShadows(); 157 } 158 159 private void initFromAttributes(Context context, AttributeSet attrs) { 160 if (attrs == null) { 161 return; 162 } 163 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FrameLayoutWithShadows); 164 165 setDefaultShadowResourceId(a.getResourceId( 166 R.styleable.FrameLayoutWithShadows_defaultShadow, 0)); 167 setDrawableBottomResourceId(a.getResourceId( 168 R.styleable.FrameLayoutWithShadows_drawableBottom, 0)); 169 170 a.recycle(); 171 } 172 173 public void setDefaultShadowResourceId(int id) { 174 mShadowResourceId = id; 175 } 176 177 public int getDefaultShadowResourceId() { 178 return mShadowResourceId; 179 } 180 181 public void setDrawableBottomResourceId(int id) { 182 mBottomResourceId = id; 183 } 184 185 public int getDrawableBottomResourceId() { 186 return mBottomResourceId; 187 } 188 189 public void setShadowsAlpha(float alpha) { 190 mShadowsAlpha = alpha; 191 for (int i = getChildCount() - 1; i >= 0; i--) { 192 View shadow = getChildAt(i); 193 if (shadow instanceof ShadowView) { 194 shadow.setAlpha(alpha); 195 } 196 } 197 } 198 199 /** 200 * prune shadow views whose related view was detached from FrameLayoutWithShadows 201 */ 202 private void prune() { 203 if (getWindowToken() ==null) { 204 return; 205 } 206 for (int i = getChildCount() - 1; i >= 0; i--) { 207 View shadow = getChildAt(i); 208 if (shadow instanceof ShadowView) { 209 ShadowView shadowView = (ShadowView) shadow; 210 View view = shadowView.shadowedView; 211 if (this != findParentShadowsView(view)) { 212 view.setTag(R.id.ShadowView, null); 213 shadowView.shadowedView = null; 214 removeView(shadowView); 215 addToRecycleBin(shadowView); 216 } 217 } 218 } 219 } 220 221 /** 222 * Perform a layout of the shadow views. This is done as part of the layout 223 * pass for the view but may also be triggered manually if the borders of a 224 * child view has changed. 225 */ 226 public void layoutShadows() { 227 prune(); 228 for (int i = getChildCount() - 1; i >= 0; i--) { 229 View shadow = getChildAt(i); 230 if (!(shadow instanceof ShadowView)) { 231 continue; 232 } 233 ShadowView shadowView = (ShadowView) shadow; 234 View view = shadowView.shadowedView; 235 if (view != null) { 236 if (this != findParentShadowsView(view)) { 237 continue; 238 } 239 boolean isImageMatrix = false; 240 if (view instanceof ImageView) { 241 // For ImageView, we get the draw bounds of the image drawable, 242 // which could be smaller than the imageView depending on ScaleType. 243 Matrix matrix = ((ImageView) view).getImageMatrix(); 244 Drawable drawable = ((ImageView) view).getDrawable(); 245 if (drawable != null) { 246 isImageMatrix = true; 247 rect.set(drawable.getBounds()); 248 rectf.set(rect); 249 matrix.mapRect(rectf); 250 rectf.offset(view.getPaddingLeft(), view.getPaddingTop()); 251 rectf.intersect(view.getPaddingLeft(), view.getPaddingTop(), 252 view.getWidth() - view.getPaddingLeft() - view.getPaddingRight(), 253 view.getHeight() - view.getPaddingTop() - view.getPaddingBottom()); 254 rectf.left -= shadow.getPaddingLeft(); 255 rectf.top -= shadow.getPaddingTop(); 256 rectf.right += shadow.getPaddingRight(); 257 rectf.bottom += shadow.getPaddingBottom(); 258 rect.left = (int) (rectf.left + 0.5f); 259 rect.top = (int) (rectf.top + 0.5f); 260 rect.right = (int) (rectf.right + 0.5f); 261 rect.bottom = (int) (rectf.bottom + 0.5f); 262 } 263 } 264 if (!isImageMatrix){ 265 rect.left = view.getPaddingLeft() - shadow.getPaddingLeft(); 266 rect.top = view.getPaddingTop() - shadow.getPaddingTop(); 267 rect.right = view.getWidth() + view.getPaddingRight() 268 + shadow.getPaddingRight(); 269 rect.bottom = view.getHeight() + view.getPaddingBottom() 270 + shadow.getPaddingBottom(); 271 } 272 offsetDescendantRectToMyCoords(view, rect); 273 shadow.layout(rect.left, rect.top, rect.right, rect.bottom); 274 } 275 } 276 } 277 278 /** 279 * Add a shadow view to FrameLayoutWithShadows. This will use the drawable 280 * specified for the shadow view and will also handle clean-up of any 281 * previous shadow set for this view. 282 */ 283 public View addShadowView(View view, Drawable shadow) { 284 ShadowView shadowView = (ShadowView) view.getTag(R.id.ShadowView); 285 if (shadowView == null) { 286 shadowView = getFromRecycleBin(); 287 if (shadowView == null) { 288 shadowView = new ShadowView(getContext()); 289 shadowView.setLayoutParams(new LayoutParams(0, 0)); 290 } 291 view.setTag(R.id.ShadowView, shadowView); 292 shadowView.shadowedView = view; 293 addView(shadowView, 0); 294 } 295 shadow.mutate(); 296 shadowView.setAlpha(mShadowsAlpha); 297 shadowView.setBackground(shadow); 298 if (mBottomResourceId != 0) { 299 Drawable d = getContext().getDrawable(mBottomResourceId); 300 shadowView.setDrawableBottom(d.mutate()); 301 } 302 return shadowView; 303 } 304 305 /** 306 * Add a shadow view using the default shadow. This will also handle 307 * clean-up of any previous shadow set for this view. 308 */ 309 public View addShadowView(View view) { 310 final Drawable shadow; 311 if (mShadowResourceId != 0) { 312 shadow = getContext().getDrawable(mShadowResourceId); 313 } else { 314 return null; 315 } 316 return addShadowView(view, shadow); 317 } 318 319 /** 320 * Get the shadow associated with the given view. Returns null if the view 321 * does not have a shadow. 322 */ 323 public static View getShadowView(View view) { 324 View shadowView = (View) view.getTag(R.id.ShadowView); 325 if (shadowView != null) { 326 return shadowView; 327 } 328 return null; 329 } 330 331 public void setShadowViewUnderline(View shadowView, int underlineColor, int heightInPx) { 332 ShapeDrawable drawable = new ShapeDrawable(); 333 drawable.setShape(new RectShape()); 334 drawable.setIntrinsicHeight(heightInPx); 335 drawable.getPaint().setColor(underlineColor); 336 ((ShadowView) shadowView).setDrawableBottom(drawable); 337 } 338 339 public void setShadowViewUnderline(View shadowView, Drawable drawable) { 340 ((ShadowView) shadowView).setDrawableBottom(drawable); 341 } 342 343 /** 344 * Makes the shadow associated with the given view draw above other views. 345 * Subsequent calls to this or changes to the z-order may move the shadow 346 * back down in the z-order. 347 */ 348 public void bringViewShadowToTop(View view) { 349 View shadowView = (View) view.getTag(R.id.ShadowView); 350 if (shadowView == null) { 351 return; 352 } 353 int index = indexOfChild(shadowView); 354 if (index < 0) { 355 // not found 356 return; 357 } 358 int lastIndex = getChildCount() - 1; 359 if (lastIndex == index) { 360 // already last one 361 return; 362 } 363 View lastShadowView = getChildAt(lastIndex); 364 if (!(lastShadowView instanceof ShadowView)) { 365 removeView(shadowView); 366 addView(shadowView); 367 } else { 368 removeView(lastShadowView); 369 removeView(shadowView); 370 addView(lastShadowView, 0); 371 addView(shadowView); 372 } 373 } 374 375 /** 376 * Utility function to remove the shadow associated with the given view. 377 */ 378 public static void removeShadowView(View view) { 379 ShadowView shadowView = (ShadowView) view.getTag(R.id.ShadowView); 380 if (shadowView != null) { 381 view.setTag(R.id.ShadowView, null); 382 shadowView.shadowedView = null; 383 if (shadowView.getRootView() != null) { 384 ViewParent parent = shadowView.getParent(); 385 if (parent instanceof ViewGroup) { 386 ((ViewGroup) parent).removeView(shadowView); 387 if (parent instanceof FrameLayoutWithShadows) { 388 ((FrameLayoutWithShadows) parent).addToRecycleBin(shadowView); 389 } 390 } 391 } 392 } 393 } 394 395 private void addToRecycleBin(ShadowView shadowView) { 396 if (mRecycleBin.size() < MAX_RECYCLE) { 397 mRecycleBin.add(shadowView); 398 } 399 } 400 401 public ShadowView getFromRecycleBin() { 402 int size = mRecycleBin.size(); 403 if (size > 0) { 404 ShadowView view = mRecycleBin.remove(size - 1); 405 view.init(); 406 } 407 return null; 408 } 409 410 /** 411 * Sets the visibility of the shadow associated with the given view. This 412 * should be called when the view's visibility changes to keep the shadow's 413 * visibility in sync. 414 */ 415 public void setShadowVisibility(View view, int visibility) { 416 View shadowView = (View) view.getTag(R.id.ShadowView); 417 if (shadowView != null) { 418 shadowView.setVisibility(visibility); 419 return; 420 } 421 } 422 423 /** 424 * Finds the first parent of this view that is a FrameLayoutWithShadows and 425 * returns that or null if there is none. 426 */ 427 public static FrameLayoutWithShadows findParentShadowsView(View view) { 428 ViewParent nextView = view.getParent(); 429 while (nextView != null && !(nextView instanceof FrameLayoutWithShadows)) { 430 nextView = nextView.getParent(); 431 } 432 return (FrameLayoutWithShadows) nextView; 433 } 434 } 435