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