/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tv.settings.widget; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.RectShape; import android.util.AttributeSet; import android.view.View; import android.view.ViewDebug.ExportedProperty; import android.view.ViewGroup; import android.view.ViewParent; import android.widget.FrameLayout; import android.widget.ImageView; import com.android.tv.settings.R; import java.util.ArrayList; /** * Allows a drawable to be added for shadowing views in this layout. The shadows * will automatically be sized to wrap their corresponding view. The default * drawable to use can be set in xml by defining the namespace and then using * defaultShadow="@drawable/reference" *

* In code views can then have Shadows added to them via * {@link #addShadowView(View)} to use the default drawable or with * {@link #addShadowView(View, Drawable)}. */ public class FrameLayoutWithShadows extends FrameLayout { private static final int MAX_RECYCLE = 12; static class ShadowView extends View { private View shadowedView; private Drawable mDrawableBottom; private float mAlpha = 1f; ShadowView(Context context) { super(context); setWillNotDraw(false); } void init() { shadowedView = null; mDrawableBottom = null; } @Override public void setBackground(Drawable background) { super.setBackground(background); if (background != null) { // framework adds a callback on background to trigger a repaint // when call Drawable.setAlpha(), this is not desired when we override // setAlpha(); if we call Drawable.setAlpha() in the overriden // setAlpha(), it will trigger another repaint event thus cause system // never stop rendering. background.setCallback(null); background.setAlpha((int)(255 * mAlpha)); } } @Override public void setAlpha(float alpha) { if (mAlpha != alpha) { mAlpha = alpha; Drawable d = getBackground(); int alphaMulitplied = (int)(alpha * 255); if (d != null) { d.setAlpha(alphaMulitplied); } if (mDrawableBottom != null) { mDrawableBottom.setAlpha(alphaMulitplied); } invalidate(); } } @Override @ExportedProperty(category = "drawing") public float getAlpha() { return mAlpha; } @Override protected boolean onSetAlpha(int alpha) { return true; } public void setDrawableBottom(Drawable drawable) { mDrawableBottom = drawable; if (mAlpha >= 0) { mDrawableBottom.setAlpha((int)(255 * mAlpha)); } invalidate(); } @Override protected void onDraw(Canvas canvas) { // draw background 9 patch super.onDraw(canvas); // draw bottom if (mDrawableBottom != null) { mDrawableBottom.setBounds(getPaddingLeft(), getHeight() - getPaddingBottom(), getWidth() - getPaddingRight(), getHeight() - getPaddingBottom() + mDrawableBottom.getIntrinsicHeight()); mDrawableBottom.draw(canvas); } } } private final Rect rect = new Rect(); private final RectF rectf = new RectF(); private int mShadowResourceId; private int mBottomResourceId; private float mShadowsAlpha = 1f; private final ArrayList mRecycleBin = new ArrayList<>(MAX_RECYCLE); public FrameLayoutWithShadows(Context context) { this(context, null); } public FrameLayoutWithShadows(Context context, AttributeSet attrs) { this(context, attrs, 0); } public FrameLayoutWithShadows(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initFromAttributes(context, attrs); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); layoutShadows(); } private void initFromAttributes(Context context, AttributeSet attrs) { if (attrs == null) { return; } TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FrameLayoutWithShadows); setDefaultShadowResourceId(a.getResourceId( R.styleable.FrameLayoutWithShadows_defaultShadow, 0)); setDrawableBottomResourceId(a.getResourceId( R.styleable.FrameLayoutWithShadows_drawableBottom, 0)); a.recycle(); } public void setDefaultShadowResourceId(int id) { mShadowResourceId = id; } public int getDefaultShadowResourceId() { return mShadowResourceId; } public void setDrawableBottomResourceId(int id) { mBottomResourceId = id; } public int getDrawableBottomResourceId() { return mBottomResourceId; } public void setShadowsAlpha(float alpha) { mShadowsAlpha = alpha; for (int i = getChildCount() - 1; i >= 0; i--) { View shadow = getChildAt(i); if (shadow instanceof ShadowView) { shadow.setAlpha(alpha); } } } /** * prune shadow views whose related view was detached from FrameLayoutWithShadows */ private void prune() { if (getWindowToken() ==null) { return; } for (int i = getChildCount() - 1; i >= 0; i--) { View shadow = getChildAt(i); if (shadow instanceof ShadowView) { ShadowView shadowView = (ShadowView) shadow; View view = shadowView.shadowedView; if (this != findParentShadowsView(view)) { view.setTag(R.id.ShadowView, null); shadowView.shadowedView = null; removeView(shadowView); addToRecycleBin(shadowView); } } } } /** * Perform a layout of the shadow views. This is done as part of the layout * pass for the view but may also be triggered manually if the borders of a * child view has changed. */ public void layoutShadows() { prune(); for (int i = getChildCount() - 1; i >= 0; i--) { View shadow = getChildAt(i); if (!(shadow instanceof ShadowView)) { continue; } ShadowView shadowView = (ShadowView) shadow; View view = shadowView.shadowedView; if (view != null) { if (this != findParentShadowsView(view)) { continue; } boolean isImageMatrix = false; if (view instanceof ImageView) { // For ImageView, we get the draw bounds of the image drawable, // which could be smaller than the imageView depending on ScaleType. Matrix matrix = ((ImageView) view).getImageMatrix(); Drawable drawable = ((ImageView) view).getDrawable(); if (drawable != null) { isImageMatrix = true; rect.set(drawable.getBounds()); rectf.set(rect); matrix.mapRect(rectf); rectf.offset(view.getPaddingLeft(), view.getPaddingTop()); rectf.intersect(view.getPaddingLeft(), view.getPaddingTop(), view.getWidth() - view.getPaddingLeft() - view.getPaddingRight(), view.getHeight() - view.getPaddingTop() - view.getPaddingBottom()); rectf.left -= shadow.getPaddingLeft(); rectf.top -= shadow.getPaddingTop(); rectf.right += shadow.getPaddingRight(); rectf.bottom += shadow.getPaddingBottom(); rect.left = (int) (rectf.left + 0.5f); rect.top = (int) (rectf.top + 0.5f); rect.right = (int) (rectf.right + 0.5f); rect.bottom = (int) (rectf.bottom + 0.5f); } } if (!isImageMatrix){ rect.left = view.getPaddingLeft() - shadow.getPaddingLeft(); rect.top = view.getPaddingTop() - shadow.getPaddingTop(); rect.right = view.getWidth() + view.getPaddingRight() + shadow.getPaddingRight(); rect.bottom = view.getHeight() + view.getPaddingBottom() + shadow.getPaddingBottom(); } offsetDescendantRectToMyCoords(view, rect); shadow.layout(rect.left, rect.top, rect.right, rect.bottom); } } } /** * Add a shadow view to FrameLayoutWithShadows. This will use the drawable * specified for the shadow view and will also handle clean-up of any * previous shadow set for this view. */ public View addShadowView(View view, Drawable shadow) { ShadowView shadowView = (ShadowView) view.getTag(R.id.ShadowView); if (shadowView == null) { shadowView = getFromRecycleBin(); if (shadowView == null) { shadowView = new ShadowView(getContext()); shadowView.setLayoutParams(new LayoutParams(0, 0)); } view.setTag(R.id.ShadowView, shadowView); shadowView.shadowedView = view; addView(shadowView, 0); } shadow.mutate(); shadowView.setAlpha(mShadowsAlpha); shadowView.setBackground(shadow); if (mBottomResourceId != 0) { Drawable d = getContext().getDrawable(mBottomResourceId); shadowView.setDrawableBottom(d.mutate()); } return shadowView; } /** * Add a shadow view using the default shadow. This will also handle * clean-up of any previous shadow set for this view. */ public View addShadowView(View view) { final Drawable shadow; if (mShadowResourceId != 0) { shadow = getContext().getDrawable(mShadowResourceId); } else { return null; } return addShadowView(view, shadow); } /** * Get the shadow associated with the given view. Returns null if the view * does not have a shadow. */ public static View getShadowView(View view) { View shadowView = (View) view.getTag(R.id.ShadowView); if (shadowView != null) { return shadowView; } return null; } public void setShadowViewUnderline(View shadowView, int underlineColor, int heightInPx) { ShapeDrawable drawable = new ShapeDrawable(); drawable.setShape(new RectShape()); drawable.setIntrinsicHeight(heightInPx); drawable.getPaint().setColor(underlineColor); ((ShadowView) shadowView).setDrawableBottom(drawable); } public void setShadowViewUnderline(View shadowView, Drawable drawable) { ((ShadowView) shadowView).setDrawableBottom(drawable); } /** * Makes the shadow associated with the given view draw above other views. * Subsequent calls to this or changes to the z-order may move the shadow * back down in the z-order. */ public void bringViewShadowToTop(View view) { View shadowView = (View) view.getTag(R.id.ShadowView); if (shadowView == null) { return; } int index = indexOfChild(shadowView); if (index < 0) { // not found return; } int lastIndex = getChildCount() - 1; if (lastIndex == index) { // already last one return; } View lastShadowView = getChildAt(lastIndex); if (!(lastShadowView instanceof ShadowView)) { removeView(shadowView); addView(shadowView); } else { removeView(lastShadowView); removeView(shadowView); addView(lastShadowView, 0); addView(shadowView); } } /** * Utility function to remove the shadow associated with the given view. */ public static void removeShadowView(View view) { ShadowView shadowView = (ShadowView) view.getTag(R.id.ShadowView); if (shadowView != null) { view.setTag(R.id.ShadowView, null); shadowView.shadowedView = null; if (shadowView.getRootView() != null) { ViewParent parent = shadowView.getParent(); if (parent instanceof ViewGroup) { ((ViewGroup) parent).removeView(shadowView); if (parent instanceof FrameLayoutWithShadows) { ((FrameLayoutWithShadows) parent).addToRecycleBin(shadowView); } } } } } private void addToRecycleBin(ShadowView shadowView) { if (mRecycleBin.size() < MAX_RECYCLE) { mRecycleBin.add(shadowView); } } public ShadowView getFromRecycleBin() { int size = mRecycleBin.size(); if (size > 0) { ShadowView view = mRecycleBin.remove(size - 1); view.init(); } return null; } /** * Sets the visibility of the shadow associated with the given view. This * should be called when the view's visibility changes to keep the shadow's * visibility in sync. */ public void setShadowVisibility(View view, int visibility) { View shadowView = (View) view.getTag(R.id.ShadowView); if (shadowView != null) { shadowView.setVisibility(visibility); return; } } /** * Finds the first parent of this view that is a FrameLayoutWithShadows and * returns that or null if there is none. */ public static FrameLayoutWithShadows findParentShadowsView(View view) { ViewParent nextView = view.getParent(); while (nextView != null && !(nextView instanceof FrameLayoutWithShadows)) { nextView = nextView.getParent(); } return (FrameLayoutWithShadows) nextView; } }