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