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