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"); 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.content.res.TypedArray;
     18 import android.graphics.Bitmap;
     19 import android.graphics.Canvas;
     20 import android.graphics.Color;
     21 import android.graphics.LinearGradient;
     22 import android.graphics.Paint;
     23 import android.graphics.PorterDuff;
     24 import android.graphics.PorterDuffXfermode;
     25 import android.graphics.Rect;
     26 import android.graphics.Shader;
     27 import android.util.AttributeSet;
     28 import android.util.TypedValue;
     29 import android.view.View;
     30 
     31 import androidx.leanback.R;
     32 import androidx.recyclerview.widget.RecyclerView;
     33 
     34 /**
     35  * A {@link android.view.ViewGroup} that shows items in a horizontal scrolling list. The items come from
     36  * the {@link RecyclerView.Adapter} associated with this view.
     37  * <p>
     38  * {@link RecyclerView.Adapter} can optionally implement {@link FacetProviderAdapter} which
     39  * provides {@link FacetProvider} for a given view type;  {@link RecyclerView.ViewHolder}
     40  * can also implement {@link FacetProvider}.  Facet from ViewHolder
     41  * has a higher priority than the one from FacetProviderAdapter associated with viewType.
     42  * Supported optional facets are:
     43  * <ol>
     44  * <li> {@link ItemAlignmentFacet}
     45  * When this facet is provided by ViewHolder or FacetProviderAdapter,  it will
     46  * override the item alignment settings set on HorizontalGridView.  This facet also allows multiple
     47  * alignment positions within one ViewHolder.
     48  * </li>
     49  * </ol>
     50  */
     51 public class HorizontalGridView extends BaseGridView {
     52 
     53     private boolean mFadingLowEdge;
     54     private boolean mFadingHighEdge;
     55 
     56     private Paint mTempPaint = new Paint();
     57     private Bitmap mTempBitmapLow;
     58     private LinearGradient mLowFadeShader;
     59     private int mLowFadeShaderLength;
     60     private int mLowFadeShaderOffset;
     61     private Bitmap mTempBitmapHigh;
     62     private LinearGradient mHighFadeShader;
     63     private int mHighFadeShaderLength;
     64     private int mHighFadeShaderOffset;
     65     private Rect mTempRect = new Rect();
     66 
     67     public HorizontalGridView(Context context) {
     68         this(context, null);
     69     }
     70 
     71     public HorizontalGridView(Context context, AttributeSet attrs) {
     72         this(context, attrs, 0);
     73     }
     74 
     75     public HorizontalGridView(Context context, AttributeSet attrs, int defStyle) {
     76         super(context, attrs, defStyle);
     77         mLayoutManager.setOrientation(RecyclerView.HORIZONTAL);
     78         initAttributes(context, attrs);
     79     }
     80 
     81     protected void initAttributes(Context context, AttributeSet attrs) {
     82         initBaseGridViewAttributes(context, attrs);
     83         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.lbHorizontalGridView);
     84         setRowHeight(a);
     85         setNumRows(a.getInt(R.styleable.lbHorizontalGridView_numberOfRows, 1));
     86         a.recycle();
     87         updateLayerType();
     88         mTempPaint = new Paint();
     89         mTempPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
     90     }
     91 
     92     void setRowHeight(TypedArray array) {
     93         TypedValue typedValue = array.peekValue(R.styleable.lbHorizontalGridView_rowHeight);
     94         if (typedValue != null) {
     95             int size = array.getLayoutDimension(R.styleable.lbHorizontalGridView_rowHeight, 0);
     96             setRowHeight(size);
     97         }
     98     }
     99 
    100     /**
    101      * Sets the number of rows.  Defaults to one.
    102      */
    103     public void setNumRows(int numRows) {
    104         mLayoutManager.setNumRows(numRows);
    105         requestLayout();
    106     }
    107 
    108     /**
    109      * Sets the row height.
    110      *
    111      * @param height May be {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT WRAP_CONTENT},
    112      *               or a size in pixels. If zero, row height will be fixed based on number of
    113      *               rows and view height.
    114      */
    115     public void setRowHeight(int height) {
    116         mLayoutManager.setRowHeight(height);
    117         requestLayout();
    118     }
    119 
    120     /**
    121      * Sets the fade out left edge to transparent.   Note turn on fading edge is very expensive
    122      * that you should turn off when HorizontalGridView is scrolling.
    123      */
    124     public final void setFadingLeftEdge(boolean fading) {
    125         if (mFadingLowEdge != fading) {
    126             mFadingLowEdge = fading;
    127             if (!mFadingLowEdge) {
    128                 mTempBitmapLow = null;
    129             }
    130             invalidate();
    131             updateLayerType();
    132         }
    133     }
    134 
    135     /**
    136      * Returns true if left edge fading is enabled.
    137      */
    138     public final boolean getFadingLeftEdge() {
    139         return mFadingLowEdge;
    140     }
    141 
    142     /**
    143      * Sets the left edge fading length in pixels.
    144      */
    145     public final void setFadingLeftEdgeLength(int fadeLength) {
    146         if (mLowFadeShaderLength != fadeLength) {
    147             mLowFadeShaderLength = fadeLength;
    148             if (mLowFadeShaderLength != 0) {
    149                 mLowFadeShader = new LinearGradient(0, 0, mLowFadeShaderLength, 0,
    150                         Color.TRANSPARENT, Color.BLACK, Shader.TileMode.CLAMP);
    151             } else {
    152                 mLowFadeShader = null;
    153             }
    154             invalidate();
    155         }
    156     }
    157 
    158     /**
    159      * Returns the left edge fading length in pixels.
    160      */
    161     public final int getFadingLeftEdgeLength() {
    162         return mLowFadeShaderLength;
    163     }
    164 
    165     /**
    166      * Sets the distance in pixels between fading start position and left padding edge.
    167      * The fading start position is positive when start position is inside left padding
    168      * area.  Default value is 0, means that the fading starts from left padding edge.
    169      */
    170     public final void setFadingLeftEdgeOffset(int fadeOffset) {
    171         if (mLowFadeShaderOffset != fadeOffset) {
    172             mLowFadeShaderOffset = fadeOffset;
    173             invalidate();
    174         }
    175     }
    176 
    177     /**
    178      * Returns the distance in pixels between fading start position and left padding edge.
    179      * The fading start position is positive when start position is inside left padding
    180      * area.  Default value is 0, means that the fading starts from left padding edge.
    181      */
    182     public final int getFadingLeftEdgeOffset() {
    183         return mLowFadeShaderOffset;
    184     }
    185 
    186     /**
    187      * Sets the fade out right edge to transparent.   Note turn on fading edge is very expensive
    188      * that you should turn off when HorizontalGridView is scrolling.
    189      */
    190     public final void setFadingRightEdge(boolean fading) {
    191         if (mFadingHighEdge != fading) {
    192             mFadingHighEdge = fading;
    193             if (!mFadingHighEdge) {
    194                 mTempBitmapHigh = null;
    195             }
    196             invalidate();
    197             updateLayerType();
    198         }
    199     }
    200 
    201     /**
    202      * Returns true if fading right edge is enabled.
    203      */
    204     public final boolean getFadingRightEdge() {
    205         return mFadingHighEdge;
    206     }
    207 
    208     /**
    209      * Sets the right edge fading length in pixels.
    210      */
    211     public final void setFadingRightEdgeLength(int fadeLength) {
    212         if (mHighFadeShaderLength != fadeLength) {
    213             mHighFadeShaderLength = fadeLength;
    214             if (mHighFadeShaderLength != 0) {
    215                 mHighFadeShader = new LinearGradient(0, 0, mHighFadeShaderLength, 0,
    216                         Color.BLACK, Color.TRANSPARENT, Shader.TileMode.CLAMP);
    217             } else {
    218                 mHighFadeShader = null;
    219             }
    220             invalidate();
    221         }
    222     }
    223 
    224     /**
    225      * Returns the right edge fading length in pixels.
    226      */
    227     public final int getFadingRightEdgeLength() {
    228         return mHighFadeShaderLength;
    229     }
    230 
    231     /**
    232      * Returns the distance in pixels between fading start position and right padding edge.
    233      * The fading start position is positive when start position is inside right padding
    234      * area.  Default value is 0, means that the fading starts from right padding edge.
    235      */
    236     public final void setFadingRightEdgeOffset(int fadeOffset) {
    237         if (mHighFadeShaderOffset != fadeOffset) {
    238             mHighFadeShaderOffset = fadeOffset;
    239             invalidate();
    240         }
    241     }
    242 
    243     /**
    244      * Sets the distance in pixels between fading start position and right padding edge.
    245      * The fading start position is positive when start position is inside right padding
    246      * area.  Default value is 0, means that the fading starts from right padding edge.
    247      */
    248     public final int getFadingRightEdgeOffset() {
    249         return mHighFadeShaderOffset;
    250     }
    251 
    252     private boolean needsFadingLowEdge() {
    253         if (!mFadingLowEdge) {
    254             return false;
    255         }
    256         final int c = getChildCount();
    257         for (int i = 0; i < c; i++) {
    258             View view = getChildAt(i);
    259             if (mLayoutManager.getOpticalLeft(view) < getPaddingLeft() - mLowFadeShaderOffset) {
    260                 return true;
    261             }
    262         }
    263         return false;
    264     }
    265 
    266     private boolean needsFadingHighEdge() {
    267         if (!mFadingHighEdge) {
    268             return false;
    269         }
    270         final int c = getChildCount();
    271         for (int i = c - 1; i >= 0; i--) {
    272             View view = getChildAt(i);
    273             if (mLayoutManager.getOpticalRight(view) > getWidth()
    274                     - getPaddingRight() + mHighFadeShaderOffset) {
    275                 return true;
    276             }
    277         }
    278         return false;
    279     }
    280 
    281     private Bitmap getTempBitmapLow() {
    282         if (mTempBitmapLow == null
    283                 || mTempBitmapLow.getWidth() != mLowFadeShaderLength
    284                 || mTempBitmapLow.getHeight() != getHeight()) {
    285             mTempBitmapLow = Bitmap.createBitmap(mLowFadeShaderLength, getHeight(),
    286                     Bitmap.Config.ARGB_8888);
    287         }
    288         return mTempBitmapLow;
    289     }
    290 
    291     private Bitmap getTempBitmapHigh() {
    292         if (mTempBitmapHigh == null
    293                 || mTempBitmapHigh.getWidth() != mHighFadeShaderLength
    294                 || mTempBitmapHigh.getHeight() != getHeight()) {
    295             // TODO: fix logic for sharing mTempBitmapLow
    296             if (false && mTempBitmapLow != null
    297                     && mTempBitmapLow.getWidth() == mHighFadeShaderLength
    298                     && mTempBitmapLow.getHeight() == getHeight()) {
    299                 // share same bitmap for low edge fading and high edge fading.
    300                 mTempBitmapHigh = mTempBitmapLow;
    301             } else {
    302                 mTempBitmapHigh = Bitmap.createBitmap(mHighFadeShaderLength, getHeight(),
    303                         Bitmap.Config.ARGB_8888);
    304             }
    305         }
    306         return mTempBitmapHigh;
    307     }
    308 
    309     @Override
    310     public void draw(Canvas canvas) {
    311         final boolean needsFadingLow = needsFadingLowEdge();
    312         final boolean needsFadingHigh = needsFadingHighEdge();
    313         if (!needsFadingLow) {
    314             mTempBitmapLow = null;
    315         }
    316         if (!needsFadingHigh) {
    317             mTempBitmapHigh = null;
    318         }
    319         if (!needsFadingLow && !needsFadingHigh) {
    320             super.draw(canvas);
    321             return;
    322         }
    323 
    324         int lowEdge = mFadingLowEdge? getPaddingLeft() - mLowFadeShaderOffset - mLowFadeShaderLength : 0;
    325         int highEdge = mFadingHighEdge ? getWidth() - getPaddingRight()
    326                 + mHighFadeShaderOffset + mHighFadeShaderLength : getWidth();
    327 
    328         // draw not-fade content
    329         int save = canvas.save();
    330         canvas.clipRect(lowEdge + (mFadingLowEdge ? mLowFadeShaderLength : 0), 0,
    331                 highEdge - (mFadingHighEdge ? mHighFadeShaderLength : 0), getHeight());
    332         super.draw(canvas);
    333         canvas.restoreToCount(save);
    334 
    335         Canvas tmpCanvas = new Canvas();
    336         mTempRect.top = 0;
    337         mTempRect.bottom = getHeight();
    338         if (needsFadingLow && mLowFadeShaderLength > 0) {
    339             Bitmap tempBitmap = getTempBitmapLow();
    340             tempBitmap.eraseColor(Color.TRANSPARENT);
    341             tmpCanvas.setBitmap(tempBitmap);
    342             // draw original content
    343             int tmpSave = tmpCanvas.save();
    344             tmpCanvas.clipRect(0, 0, mLowFadeShaderLength, getHeight());
    345             tmpCanvas.translate(-lowEdge, 0);
    346             super.draw(tmpCanvas);
    347             tmpCanvas.restoreToCount(tmpSave);
    348             // draw fading out
    349             mTempPaint.setShader(mLowFadeShader);
    350             tmpCanvas.drawRect(0, 0, mLowFadeShaderLength, getHeight(), mTempPaint);
    351             // copy back to canvas
    352             mTempRect.left = 0;
    353             mTempRect.right = mLowFadeShaderLength;
    354             canvas.translate(lowEdge, 0);
    355             canvas.drawBitmap(tempBitmap, mTempRect, mTempRect, null);
    356             canvas.translate(-lowEdge, 0);
    357         }
    358         if (needsFadingHigh && mHighFadeShaderLength > 0) {
    359             Bitmap tempBitmap = getTempBitmapHigh();
    360             tempBitmap.eraseColor(Color.TRANSPARENT);
    361             tmpCanvas.setBitmap(tempBitmap);
    362             // draw original content
    363             int tmpSave = tmpCanvas.save();
    364             tmpCanvas.clipRect(0, 0, mHighFadeShaderLength, getHeight());
    365             tmpCanvas.translate(-(highEdge - mHighFadeShaderLength), 0);
    366             super.draw(tmpCanvas);
    367             tmpCanvas.restoreToCount(tmpSave);
    368             // draw fading out
    369             mTempPaint.setShader(mHighFadeShader);
    370             tmpCanvas.drawRect(0, 0, mHighFadeShaderLength, getHeight(), mTempPaint);
    371             // copy back to canvas
    372             mTempRect.left = 0;
    373             mTempRect.right = mHighFadeShaderLength;
    374             canvas.translate(highEdge - mHighFadeShaderLength, 0);
    375             canvas.drawBitmap(tempBitmap, mTempRect, mTempRect, null);
    376             canvas.translate(-(highEdge - mHighFadeShaderLength), 0);
    377         }
    378     }
    379 
    380     /**
    381      * Updates the layer type for this view.
    382      * If fading edges are needed, use a hardware layer.  This works around the problem
    383      * that when a child invalidates itself (for example has an animated background),
    384      * the parent view must also be invalidated to refresh the display list which
    385      * updates the the caching bitmaps used to draw the fading edges.
    386      */
    387     private void updateLayerType() {
    388         if (mFadingLowEdge || mFadingHighEdge) {
    389             setLayerType(View.LAYER_TYPE_HARDWARE, null);
    390             setWillNotDraw(false);
    391         } else {
    392             setLayerType(View.LAYER_TYPE_NONE, null);
    393             setWillNotDraw(true);
    394         }
    395     }
    396 }
    397