Home | History | Annotate | Download | only in widget
      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