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 FacetProiderAdapter 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) <
259                     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