BaseGridView.java revision 95400e6d31a1ac09e48cb8944a79b7250484aa4c
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.Rect;
19import android.support.v17.leanback.R;
20import android.support.v7.widget.RecyclerView;
21import android.util.AttributeSet;
22import android.view.Gravity;
23import android.view.KeyEvent;
24import android.view.MotionEvent;
25import android.view.View;
26
27/**
28 * Base class for vertically and horizontally scrolling lists. The items come
29 * from the {@link RecyclerView.Adapter} associated with this view.
30 * @hide
31 */
32abstract class BaseGridView extends RecyclerView {
33
34    /**
35     * Always keep focused item at a aligned position.  Developer can use
36     * WINDOW_ALIGN_XXX and ITEM_ALIGN_XXX to define how focused item is aligned.
37     * In this mode, the last focused position will be remembered and restored when focus
38     * is back to the view.
39     */
40    public final static int FOCUS_SCROLL_ALIGNED = 0;
41
42    /**
43     * Scroll to make the focused item inside client area.
44     */
45    public final static int FOCUS_SCROLL_ITEM = 1;
46
47    /**
48     * Scroll a page of items when focusing to item outside the client area.
49     * The page size matches the client area size of RecyclerView.
50     */
51    public final static int FOCUS_SCROLL_PAGE = 2;
52
53    /**
54     * The first item is aligned with the low edge of the viewport. When
55     * navigating away from the first item, the focus maintains a middle
56     * location.
57     * <p>
58     * The middle location is calculated by "windowAlignOffset" and
59     * "windowAlignOffsetPercent"; if neither of these two is defined, the
60     * default value is 1/2 of the size.
61     */
62    public final static int WINDOW_ALIGN_LOW_EDGE = 1;
63
64    /**
65     * The last item is aligned with the high edge of the viewport when
66     * navigating to the end of list. When navigating away from the end, the
67     * focus maintains a middle location.
68     * <p>
69     * The middle location is calculated by "windowAlignOffset" and
70     * "windowAlignOffsetPercent"; if neither of these two is defined, the
71     * default value is 1/2 of the size.
72     */
73    public final static int WINDOW_ALIGN_HIGH_EDGE = 1 << 1;
74
75    /**
76     * The first item and last item are aligned with the two edges of the
77     * viewport. When navigating in the middle of list, the focus maintains a
78     * middle location.
79     * <p>
80     * The middle location is calculated by "windowAlignOffset" and
81     * "windowAlignOffsetPercent"; if neither of these two is defined, the
82     * default value is 1/2 of the size.
83     */
84    public final static int WINDOW_ALIGN_BOTH_EDGE =
85            WINDOW_ALIGN_LOW_EDGE | WINDOW_ALIGN_HIGH_EDGE;
86
87    /**
88     * The focused item always stays in a middle location.
89     * <p>
90     * The middle location is calculated by "windowAlignOffset" and
91     * "windowAlignOffsetPercent"; if neither of these two is defined, the
92     * default value is 1/2 of the size.
93     */
94    public final static int WINDOW_ALIGN_NO_EDGE = 0;
95
96    /**
97     * Value indicates that percent is not used.
98     */
99    public final static float WINDOW_ALIGN_OFFSET_PERCENT_DISABLED = -1;
100
101    /**
102     * Value indicates that percent is not used.
103     */
104    public final static float ITEM_ALIGN_OFFSET_PERCENT_DISABLED = -1;
105
106    /**
107     * Listener for intercepting touch dispatch events.
108     */
109    public interface OnTouchInterceptListener {
110        /**
111         * Returns true if the touch dispatch event should be consumed.
112         */
113        public boolean onInterceptTouchEvent(MotionEvent event);
114    }
115
116    /**
117     * Listener for intercepting generic motion dispatch events.
118     */
119    public interface OnMotionInterceptListener {
120        /**
121         * Returns true if the touch dispatch event should be consumed.
122         */
123        public boolean onInterceptMotionEvent(MotionEvent event);
124    }
125
126    /**
127     * Listener for intercepting key dispatch events.
128     */
129    public interface OnKeyInterceptListener {
130        /**
131         * Returns true if the key dispatch event should be consumed.
132         */
133        public boolean onInterceptKeyEvent(KeyEvent event);
134    }
135
136    protected final GridLayoutManager mLayoutManager;
137
138    /**
139     * Animate layout changes from a child resizing or adding/removing a child.
140     */
141    private boolean mAnimateChildLayout = true;
142
143    private RecyclerView.ItemAnimator mSavedItemAnimator;
144
145    private OnTouchInterceptListener mOnTouchInterceptListener;
146    private OnMotionInterceptListener mOnMotionInterceptListener;
147    private OnKeyInterceptListener mOnKeyInterceptListener;
148
149    public BaseGridView(Context context, AttributeSet attrs, int defStyle) {
150        super(context, attrs, defStyle);
151        mLayoutManager = new GridLayoutManager(this);
152        setLayoutManager(mLayoutManager);
153        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
154        setHasFixedSize(true);
155        setChildrenDrawingOrderEnabled(true);
156    }
157
158    protected void initBaseGridViewAttributes(Context context, AttributeSet attrs) {
159        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.lbBaseGridView);
160        boolean throughFront = a.getBoolean(R.styleable.lbBaseGridView_focusOutFront, false);
161        boolean throughEnd = a.getBoolean(R.styleable.lbBaseGridView_focusOutEnd, false);
162        mLayoutManager.setFocusOutAllowed(throughFront, throughEnd);
163        mLayoutManager.setVerticalMargin(
164                a.getDimensionPixelSize(R.styleable.lbBaseGridView_verticalMargin, 0));
165        mLayoutManager.setHorizontalMargin(
166                a.getDimensionPixelSize(R.styleable.lbBaseGridView_horizontalMargin, 0));
167        if (a.hasValue(R.styleable.lbBaseGridView_android_gravity)) {
168            setGravity(a.getInt(R.styleable.lbBaseGridView_android_gravity, Gravity.NO_GRAVITY));
169        }
170        a.recycle();
171    }
172
173    /**
174     * Set the strategy used to scroll in response to item focus changing:
175     * <ul>
176     * <li>{@link #FOCUS_SCROLL_ALIGNED} (default) </li>
177     * <li>{@link #FOCUS_SCROLL_ITEM}</li>
178     * <li>{@link #FOCUS_SCROLL_PAGE}</li>
179     * </ul>
180     */
181    public void setFocusScrollStrategy(int scrollStrategy) {
182        if (scrollStrategy != FOCUS_SCROLL_ALIGNED && scrollStrategy != FOCUS_SCROLL_ITEM
183            && scrollStrategy != FOCUS_SCROLL_PAGE) {
184            throw new IllegalArgumentException("Invalid scrollStrategy");
185        }
186        mLayoutManager.setFocusScrollStrategy(scrollStrategy);
187        requestLayout();
188    }
189
190    /**
191     * Returns the strategy used to scroll in response to item focus changing.
192     * <ul>
193     * <li>{@link #FOCUS_SCROLL_ALIGNED} (default) </li>
194     * <li>{@link #FOCUS_SCROLL_ITEM}</li>
195     * <li>{@link #FOCUS_SCROLL_PAGE}</li>
196     * </ul>
197     */
198    public int getFocusScrollStrategy() {
199        return mLayoutManager.getFocusScrollStrategy();
200    }
201
202    /**
203     * Set how the focused item gets aligned in the view.
204     *
205     * @param windowAlignment {@link #WINDOW_ALIGN_BOTH_EDGE},
206     *        {@link #WINDOW_ALIGN_LOW_EDGE}, {@link #WINDOW_ALIGN_HIGH_EDGE} or
207     *        {@link #WINDOW_ALIGN_NO_EDGE}.
208     */
209    public void setWindowAlignment(int windowAlignment) {
210        mLayoutManager.setWindowAlignment(windowAlignment);
211        requestLayout();
212    }
213
214    /**
215     * Get how the focused item gets aligned in the view.
216     *
217     * @return {@link #WINDOW_ALIGN_BOTH_EDGE}, {@link #WINDOW_ALIGN_LOW_EDGE},
218     *         {@link #WINDOW_ALIGN_HIGH_EDGE} or {@link #WINDOW_ALIGN_NO_EDGE}.
219     */
220    public int getWindowAlignment() {
221        return mLayoutManager.getWindowAlignment();
222    }
223
224    /**
225     * Set the absolute offset in pixels for window alignment.
226     *
227     * @param offset The number of pixels to offset. Can be negative for
228     *        alignment from the high edge, or positive for alignment from the
229     *        low edge.
230     */
231    public void setWindowAlignmentOffset(int offset) {
232        mLayoutManager.setWindowAlignmentOffset(offset);
233        requestLayout();
234    }
235
236    /**
237     * Get the absolute offset in pixels for window alignment.
238     *
239     * @return The number of pixels to offset. Will be negative for alignment
240     *         from the high edge, or positive for alignment from the low edge.
241     *         Default value is 0.
242     */
243    public int getWindowAlignmentOffset() {
244        return mLayoutManager.getWindowAlignmentOffset();
245    }
246
247    /**
248     * Set offset percent for window alignment in addition to {@link
249     * #getWindowAlignmentOffset()}.
250     *
251     * @param offsetPercent Percentage to offset. E.g., 40 means 40% of the
252     *        width from low edge. Use
253     *        {@link #WINDOW_ALIGN_OFFSET_PERCENT_DISABLED} to disable.
254     */
255    public void setWindowAlignmentOffsetPercent(float offsetPercent) {
256        mLayoutManager.setWindowAlignmentOffsetPercent(offsetPercent);
257        requestLayout();
258    }
259
260    /**
261     * Get offset percent for window alignment in addition to
262     * {@link #getWindowAlignmentOffset()}.
263     *
264     * @return Percentage to offset. E.g., 40 means 40% of the width from the
265     *         low edge, or {@link #WINDOW_ALIGN_OFFSET_PERCENT_DISABLED} if
266     *         disabled. Default value is 50.
267     */
268    public float getWindowAlignmentOffsetPercent() {
269        return mLayoutManager.getWindowAlignmentOffsetPercent();
270    }
271
272    /**
273     * Set the absolute offset in pixels for item alignment.
274     *
275     * @param offset The number of pixels to offset. Can be negative for
276     *        alignment from the high edge, or positive for alignment from the
277     *        low edge.
278     */
279    public void setItemAlignmentOffset(int offset) {
280        mLayoutManager.setItemAlignmentOffset(offset);
281        requestLayout();
282    }
283
284    /**
285     * Get the absolute offset in pixels for item alignment.
286     *
287     * @return The number of pixels to offset. Will be negative for alignment
288     *         from the high edge, or positive for alignment from the low edge.
289     *         Default value is 0.
290     */
291    public int getItemAlignmentOffset() {
292        return mLayoutManager.getItemAlignmentOffset();
293    }
294
295    /**
296     * Set to true if include padding in calculating item align offset.
297     *
298     * @param withPadding When it is true: we include left/top padding for positive
299     *          item offset, include right/bottom padding for negative item offset.
300     */
301    public void setItemAlignmentOffsetWithPadding(boolean withPadding) {
302        mLayoutManager.setItemAlignmentOffsetWithPadding(withPadding);
303        requestLayout();
304    }
305
306    /**
307     * Returns true if include padding in calculating item align offset.
308     */
309    public boolean isItemAlignmentOffsetWithPadding() {
310        return mLayoutManager.isItemAlignmentOffsetWithPadding();
311    }
312
313    /**
314     * Set offset percent for item alignment in addition to {@link
315     * #getItemAlignmentOffset()}.
316     *
317     * @param offsetPercent Percentage to offset. E.g., 40 means 40% of the
318     *        width from the low edge. Use
319     *        {@link #ITEM_ALIGN_OFFSET_PERCENT_DISABLED} to disable.
320     */
321    public void setItemAlignmentOffsetPercent(float offsetPercent) {
322        mLayoutManager.setItemAlignmentOffsetPercent(offsetPercent);
323        requestLayout();
324    }
325
326    /**
327     * Get offset percent for item alignment in addition to {@link
328     * #getItemAlignmentOffset()}.
329     *
330     * @return Percentage to offset. E.g., 40 means 40% of the width from the
331     *         low edge, or {@link #ITEM_ALIGN_OFFSET_PERCENT_DISABLED} if
332     *         disabled. Default value is 50.
333     */
334    public float getItemAlignmentOffsetPercent() {
335        return mLayoutManager.getItemAlignmentOffsetPercent();
336    }
337
338    /**
339     * Set the id of the view to align with. Use zero (default) for the item
340     * view itself.
341     */
342    public void setItemAlignmentViewId(int viewId) {
343        mLayoutManager.setItemAlignmentViewId(viewId);
344    }
345
346    /**
347     * Get the id of the view to align with, or zero for the item view itself.
348     */
349    public int getItemAlignmentViewId() {
350        return mLayoutManager.getItemAlignmentViewId();
351    }
352
353    /**
354     * Set the margin in pixels between two child items.
355     */
356    public void setItemMargin(int margin) {
357        mLayoutManager.setItemMargin(margin);
358        requestLayout();
359    }
360
361    /**
362     * Set the margin in pixels between two child items vertically.
363     */
364    public void setVerticalMargin(int margin) {
365        mLayoutManager.setVerticalMargin(margin);
366        requestLayout();
367    }
368
369    /**
370     * Get the margin in pixels between two child items vertically.
371     */
372    public int getVerticalMargin() {
373        return mLayoutManager.getVerticalMargin();
374    }
375
376    /**
377     * Set the margin in pixels between two child items horizontally.
378     */
379    public void setHorizontalMargin(int margin) {
380        mLayoutManager.setHorizontalMargin(margin);
381        requestLayout();
382    }
383
384    /**
385     * Get the margin in pixels between two child items horizontally.
386     */
387    public int getHorizontalMargin() {
388        return mLayoutManager.getHorizontalMargin();
389    }
390
391    /**
392     * Register a callback to be invoked when an item in BaseGridView has
393     * been selected.
394     */
395    public void setOnChildSelectedListener(OnChildSelectedListener listener) {
396        mLayoutManager.setOnChildSelectedListener(listener);
397    }
398
399    /**
400     * Change the selected item immediately without animation.
401     */
402    public void setSelectedPosition(int position) {
403        mLayoutManager.setSelection(this, position);
404    }
405
406    /**
407     * Change the selected item and run an animation to scroll to the target
408     * position.
409     */
410    public void setSelectedPositionSmooth(int position) {
411        mLayoutManager.setSelectionSmooth(this, position);
412    }
413
414    /**
415     * Get the selected item position.
416     */
417    public int getSelectedPosition() {
418        return mLayoutManager.getSelection();
419    }
420
421    /**
422     * Set if an animation should run when a child changes size or when adding
423     * or removing a child.
424     * <p><i>Unstable API, might change later.</i>
425     */
426    public void setAnimateChildLayout(boolean animateChildLayout) {
427        if (mAnimateChildLayout != animateChildLayout) {
428            mAnimateChildLayout = animateChildLayout;
429            if (!mAnimateChildLayout) {
430                mSavedItemAnimator = getItemAnimator();
431                super.setItemAnimator(null);
432            } else {
433                super.setItemAnimator(mSavedItemAnimator);
434            }
435        }
436    }
437
438    /**
439     * Return true if an animation will run when a child changes size or when
440     * adding or removing a child.
441     * <p><i>Unstable API, might change later.</i>
442     */
443    public boolean isChildLayoutAnimated() {
444        return mAnimateChildLayout;
445    }
446
447    /**
448     * Describes how the child views are positioned. Defaults to
449     * GRAVITY_TOP|GRAVITY_LEFT.
450     *
451     * @param gravity See {@link android.view.Gravity}
452     */
453    public void setGravity(int gravity) {
454        mLayoutManager.setGravity(gravity);
455        requestLayout();
456    }
457
458    @Override
459    public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
460        return mLayoutManager.gridOnRequestFocusInDescendants(this, direction,
461                previouslyFocusedRect);
462    }
463
464    /**
465     * Get the x/y offsets to final position from current position if the view
466     * is selected.
467     *
468     * @param view The view to get offsets.
469     * @param offsets offsets[0] holds offset of X, offsets[1] holds offset of
470     *        Y.
471     */
472    public void getViewSelectedOffsets(View view, int[] offsets) {
473        mLayoutManager.getViewSelectedOffsets(view, offsets);
474    }
475
476    @Override
477    public int getChildDrawingOrder(int childCount, int i) {
478        return mLayoutManager.getChildDrawingOrder(this, childCount, i);
479    }
480
481    final boolean isChildrenDrawingOrderEnabledInternal() {
482        return isChildrenDrawingOrderEnabled();
483    }
484
485    /**
486     * Disable or enable focus search.
487     */
488    public final void setFocusSearchDisabled(boolean disabled) {
489        mLayoutManager.setFocusSearchDisabled(disabled);
490    }
491
492    /**
493     * Return true if focus search is disabled.
494     */
495    public final boolean isFocusSearchDisabled() {
496        return mLayoutManager.isFocusSearchDisabled();
497    }
498
499    /**
500     * Enable or disable layout.  All children will be removed when layout is
501     * disabled.
502     */
503    public void setLayoutEnabled(boolean layoutEnabled) {
504        mLayoutManager.setLayoutEnabled(layoutEnabled);
505    }
506
507    /**
508     * Enable or disable pruning child.  Disable is useful during transition.
509     */
510    public void setPruneChild(boolean pruneChild) {
511        mLayoutManager.setPruneChild(pruneChild);
512    }
513
514    /**
515     * Returns true if the view at the given position has a same row sibling
516     * in front of it.
517     *
518     * @param position Position in adapter.
519     */
520    public boolean hasPreviousViewInSameRow(int position) {
521        return mLayoutManager.hasPreviousViewInSameRow(position);
522    }
523
524    /**
525     * Enable or disable the default "focus draw at last" order rule.
526     */
527    public void setFocusDrawingOrderEnabled(boolean enabled) {
528        super.setChildrenDrawingOrderEnabled(enabled);
529    }
530
531    /**
532     * Returns true if default "focus draw at last" order rule is enabled.
533     */
534    public boolean isFocusDrawingOrderEnabled() {
535        return super.isChildrenDrawingOrderEnabled();
536    }
537
538    /**
539     * Sets the touch intercept listener.
540     */
541    public void setOnTouchInterceptListener(OnTouchInterceptListener listener) {
542        mOnTouchInterceptListener = listener;
543    }
544
545    /**
546     * Sets the generic motion intercept listener.
547     */
548    public void setOnMotionInterceptListener(OnMotionInterceptListener listener) {
549        mOnMotionInterceptListener = listener;
550    }
551
552    /**
553     * Sets the key intercept listener.
554     */
555    public void setOnKeyInterceptListener(OnKeyInterceptListener listener) {
556        mOnKeyInterceptListener = listener;
557    }
558
559    @Override
560    public boolean dispatchKeyEvent(KeyEvent event) {
561        if (mOnKeyInterceptListener != null) {
562            if (mOnKeyInterceptListener.onInterceptKeyEvent(event)) {
563                return true;
564            }
565        }
566        return super.dispatchKeyEvent(event);
567    }
568
569    @Override
570    public boolean dispatchTouchEvent(MotionEvent event) {
571        if (mOnTouchInterceptListener != null) {
572            if (mOnTouchInterceptListener.onInterceptTouchEvent(event)) {
573                return true;
574            }
575        }
576        return super.dispatchTouchEvent(event);
577    }
578
579    @Override
580    public boolean dispatchGenericFocusedEvent(MotionEvent event) {
581        if (mOnMotionInterceptListener != null) {
582            if (mOnMotionInterceptListener.onInterceptMotionEvent(event)) {
583                return true;
584            }
585        }
586        return super.dispatchGenericFocusedEvent(event);
587    }
588}
589