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 * <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 */
50public 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
66    public HorizontalGridView(Context context) {
67        this(context, null);
68    }
69
70    public HorizontalGridView(Context context, AttributeSet attrs) {
71        this(context, attrs, 0);
72    }
73
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
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
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     */
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     */
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     */
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     */
137    public final boolean getFadingLeftEdge() {
138        return mFadingLowEdge;
139    }
140
141    /**
142     * Sets the left edge fading length in pixels.
143     */
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     */
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     */
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     */
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     */
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     */
203    public final boolean getFadingRightEdge() {
204        return mFadingHighEdge;
205    }
206
207    /**
208     * Sets the right edge fading length in pixels.
209     */
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     */
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     */
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     */
247    public final int getFadingRightEdgeOffset() {
248        return mHighFadeShaderOffset;
249    }
250
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
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
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
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
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     */
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