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 
HorizontalGridView(Context context)67     public HorizontalGridView(Context context) {
68         this(context, null);
69     }
70 
HorizontalGridView(Context context, AttributeSet attrs)71     public HorizontalGridView(Context context, AttributeSet attrs) {
72         this(context, attrs, 0);
73     }
74 
HorizontalGridView(Context context, AttributeSet attrs, int defStyle)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 
initAttributes(Context context, AttributeSet attrs)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 
setRowHeight(TypedArray array)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      */
setNumRows(int numRows)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      */
setRowHeight(int height)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      */
setFadingLeftEdge(boolean fading)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      */
getFadingLeftEdge()138     public final boolean getFadingLeftEdge() {
139         return mFadingLowEdge;
140     }
141 
142     /**
143      * Sets the left edge fading length in pixels.
144      */
setFadingLeftEdgeLength(int fadeLength)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      */
getFadingLeftEdgeLength()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      */
setFadingLeftEdgeOffset(int fadeOffset)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      */
getFadingLeftEdgeOffset()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      */
setFadingRightEdge(boolean fading)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      */
getFadingRightEdge()204     public final boolean getFadingRightEdge() {
205         return mFadingHighEdge;
206     }
207 
208     /**
209      * Sets the right edge fading length in pixels.
210      */
setFadingRightEdgeLength(int fadeLength)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      */
getFadingRightEdgeLength()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      */
setFadingRightEdgeOffset(int fadeOffset)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      */
getFadingRightEdgeOffset()248     public final int getFadingRightEdgeOffset() {
249         return mHighFadeShaderOffset;
250     }
251 
needsFadingLowEdge()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 
needsFadingHighEdge()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 
getTempBitmapLow()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 
getTempBitmapHigh()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
draw(Canvas canvas)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      */
updateLayerType()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