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 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 */
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) <
259                    getPaddingLeft() - mLowFadeShaderOffset) {
260                return true;
261            }
262        }
263        return false;
264    }
265
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
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
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
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     */
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