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