BaseGridView.java revision 0fff85d7f9dee67ec5116f3cba4e8b3961f805a7
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     * Dont save states of any child views.
108     */
109    public static final int SAVE_NO_CHILD = 0;
110
111    /**
112     * Only save on screen child views, the states are lost when they become off screen.
113     */
114    public static final int SAVE_ON_SCREEN_CHILD = 1;
115
116    /**
117     * Save on screen views plus save off screen child views states up to
118     * {@link #getSaveChildrenLimitNumber()}.
119     */
120    public static final int SAVE_LIMITED_CHILD = 2;
121
122    /**
123     * Save on screen views plus save off screen child views without any limitation.
124     * This might cause out of memory, only use it when you are dealing with limited data.
125     */
126    public static final int SAVE_ALL_CHILD = 3;
127
128    /**
129     * Listener for intercepting touch dispatch events.
130     */
131    public interface OnTouchInterceptListener {
132        /**
133         * Returns true if the touch dispatch event should be consumed.
134         */
135        public boolean onInterceptTouchEvent(MotionEvent event);
136    }
137
138    /**
139     * Listener for intercepting generic motion dispatch events.
140     */
141    public interface OnMotionInterceptListener {
142        /**
143         * Returns true if the touch dispatch event should be consumed.
144         */
145        public boolean onInterceptMotionEvent(MotionEvent event);
146    }
147
148    /**
149     * Listener for intercepting key dispatch events.
150     */
151    public interface OnKeyInterceptListener {
152        /**
153         * Returns true if the key dispatch event should be consumed.
154         */
155        public boolean onInterceptKeyEvent(KeyEvent event);
156    }
157
158    protected final GridLayoutManager mLayoutManager;
159
160    /**
161     * Animate layout changes from a child resizing or adding/removing a child.
162     */
163    private boolean mAnimateChildLayout = true;
164
165    private RecyclerView.ItemAnimator mSavedItemAnimator;
166
167    private OnTouchInterceptListener mOnTouchInterceptListener;
168    private OnMotionInterceptListener mOnMotionInterceptListener;
169    private OnKeyInterceptListener mOnKeyInterceptListener;
170
171    public BaseGridView(Context context, AttributeSet attrs, int defStyle) {
172        super(context, attrs, defStyle);
173        mLayoutManager = new GridLayoutManager(this);
174        setLayoutManager(mLayoutManager);
175        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
176        setHasFixedSize(true);
177        setChildrenDrawingOrderEnabled(true);
178        setWillNotDraw(true);
179        setOverScrollMode(View.OVER_SCROLL_NEVER);
180    }
181
182    protected void initBaseGridViewAttributes(Context context, AttributeSet attrs) {
183        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.lbBaseGridView);
184        boolean throughFront = a.getBoolean(R.styleable.lbBaseGridView_focusOutFront, false);
185        boolean throughEnd = a.getBoolean(R.styleable.lbBaseGridView_focusOutEnd, false);
186        mLayoutManager.setFocusOutAllowed(throughFront, throughEnd);
187        mLayoutManager.setVerticalMargin(
188                a.getDimensionPixelSize(R.styleable.lbBaseGridView_verticalMargin, 0));
189        mLayoutManager.setHorizontalMargin(
190                a.getDimensionPixelSize(R.styleable.lbBaseGridView_horizontalMargin, 0));
191        if (a.hasValue(R.styleable.lbBaseGridView_android_gravity)) {
192            setGravity(a.getInt(R.styleable.lbBaseGridView_android_gravity, Gravity.NO_GRAVITY));
193        }
194        a.recycle();
195    }
196
197    /**
198     * Set the strategy used to scroll in response to item focus changing:
199     * <ul>
200     * <li>{@link #FOCUS_SCROLL_ALIGNED} (default) </li>
201     * <li>{@link #FOCUS_SCROLL_ITEM}</li>
202     * <li>{@link #FOCUS_SCROLL_PAGE}</li>
203     * </ul>
204     */
205    public void setFocusScrollStrategy(int scrollStrategy) {
206        if (scrollStrategy != FOCUS_SCROLL_ALIGNED && scrollStrategy != FOCUS_SCROLL_ITEM
207            && scrollStrategy != FOCUS_SCROLL_PAGE) {
208            throw new IllegalArgumentException("Invalid scrollStrategy");
209        }
210        mLayoutManager.setFocusScrollStrategy(scrollStrategy);
211        requestLayout();
212    }
213
214    /**
215     * Returns the strategy used to scroll in response to item focus changing.
216     * <ul>
217     * <li>{@link #FOCUS_SCROLL_ALIGNED} (default) </li>
218     * <li>{@link #FOCUS_SCROLL_ITEM}</li>
219     * <li>{@link #FOCUS_SCROLL_PAGE}</li>
220     * </ul>
221     */
222    public int getFocusScrollStrategy() {
223        return mLayoutManager.getFocusScrollStrategy();
224    }
225
226    /**
227     * Set how the focused item gets aligned in the view.
228     *
229     * @param windowAlignment {@link #WINDOW_ALIGN_BOTH_EDGE},
230     *        {@link #WINDOW_ALIGN_LOW_EDGE}, {@link #WINDOW_ALIGN_HIGH_EDGE} or
231     *        {@link #WINDOW_ALIGN_NO_EDGE}.
232     */
233    public void setWindowAlignment(int windowAlignment) {
234        mLayoutManager.setWindowAlignment(windowAlignment);
235        requestLayout();
236    }
237
238    /**
239     * Get how the focused item gets aligned in the view.
240     *
241     * @return {@link #WINDOW_ALIGN_BOTH_EDGE}, {@link #WINDOW_ALIGN_LOW_EDGE},
242     *         {@link #WINDOW_ALIGN_HIGH_EDGE} or {@link #WINDOW_ALIGN_NO_EDGE}.
243     */
244    public int getWindowAlignment() {
245        return mLayoutManager.getWindowAlignment();
246    }
247
248    /**
249     * Set the absolute offset in pixels for window alignment.
250     *
251     * @param offset The number of pixels to offset. Can be negative for
252     *        alignment from the high edge, or positive for alignment from the
253     *        low edge.
254     */
255    public void setWindowAlignmentOffset(int offset) {
256        mLayoutManager.setWindowAlignmentOffset(offset);
257        requestLayout();
258    }
259
260    /**
261     * Get the absolute offset in pixels for window alignment.
262     *
263     * @return The number of pixels to offset. Will be negative for alignment
264     *         from the high edge, or positive for alignment from the low edge.
265     *         Default value is 0.
266     */
267    public int getWindowAlignmentOffset() {
268        return mLayoutManager.getWindowAlignmentOffset();
269    }
270
271    /**
272     * Set offset percent for window alignment in addition to {@link
273     * #getWindowAlignmentOffset()}.
274     *
275     * @param offsetPercent Percentage to offset. E.g., 40 means 40% of the
276     *        width from low edge. Use
277     *        {@link #WINDOW_ALIGN_OFFSET_PERCENT_DISABLED} to disable.
278     */
279    public void setWindowAlignmentOffsetPercent(float offsetPercent) {
280        mLayoutManager.setWindowAlignmentOffsetPercent(offsetPercent);
281        requestLayout();
282    }
283
284    /**
285     * Get offset percent for window alignment in addition to
286     * {@link #getWindowAlignmentOffset()}.
287     *
288     * @return Percentage to offset. E.g., 40 means 40% of the width from the
289     *         low edge, or {@link #WINDOW_ALIGN_OFFSET_PERCENT_DISABLED} if
290     *         disabled. Default value is 50.
291     */
292    public float getWindowAlignmentOffsetPercent() {
293        return mLayoutManager.getWindowAlignmentOffsetPercent();
294    }
295
296    /**
297     * Set the absolute offset in pixels for item alignment.
298     *
299     * @param offset The number of pixels to offset. Can be negative for
300     *        alignment from the high edge, or positive for alignment from the
301     *        low edge.
302     */
303    public void setItemAlignmentOffset(int offset) {
304        mLayoutManager.setItemAlignmentOffset(offset);
305        requestLayout();
306    }
307
308    /**
309     * Get the absolute offset in pixels for item alignment.
310     *
311     * @return The number of pixels to offset. Will be negative for alignment
312     *         from the high edge, or positive for alignment from the low edge.
313     *         Default value is 0.
314     */
315    public int getItemAlignmentOffset() {
316        return mLayoutManager.getItemAlignmentOffset();
317    }
318
319    /**
320     * Set to true if include padding in calculating item align offset.
321     *
322     * @param withPadding When it is true: we include left/top padding for positive
323     *          item offset, include right/bottom padding for negative item offset.
324     */
325    public void setItemAlignmentOffsetWithPadding(boolean withPadding) {
326        mLayoutManager.setItemAlignmentOffsetWithPadding(withPadding);
327        requestLayout();
328    }
329
330    /**
331     * Returns true if include padding in calculating item align offset.
332     */
333    public boolean isItemAlignmentOffsetWithPadding() {
334        return mLayoutManager.isItemAlignmentOffsetWithPadding();
335    }
336
337    /**
338     * Set offset percent for item alignment in addition to {@link
339     * #getItemAlignmentOffset()}.
340     *
341     * @param offsetPercent Percentage to offset. E.g., 40 means 40% of the
342     *        width from the low edge. Use
343     *        {@link #ITEM_ALIGN_OFFSET_PERCENT_DISABLED} to disable.
344     */
345    public void setItemAlignmentOffsetPercent(float offsetPercent) {
346        mLayoutManager.setItemAlignmentOffsetPercent(offsetPercent);
347        requestLayout();
348    }
349
350    /**
351     * Get offset percent for item alignment in addition to {@link
352     * #getItemAlignmentOffset()}.
353     *
354     * @return Percentage to offset. E.g., 40 means 40% of the width from the
355     *         low edge, or {@link #ITEM_ALIGN_OFFSET_PERCENT_DISABLED} if
356     *         disabled. Default value is 50.
357     */
358    public float getItemAlignmentOffsetPercent() {
359        return mLayoutManager.getItemAlignmentOffsetPercent();
360    }
361
362    /**
363     * Set the id of the view to align with. Use zero (default) for the item
364     * view itself.
365     */
366    public void setItemAlignmentViewId(int viewId) {
367        mLayoutManager.setItemAlignmentViewId(viewId);
368    }
369
370    /**
371     * Get the id of the view to align with, or zero for the item view itself.
372     */
373    public int getItemAlignmentViewId() {
374        return mLayoutManager.getItemAlignmentViewId();
375    }
376
377    /**
378     * Set the margin in pixels between two child items.
379     */
380    public void setItemMargin(int margin) {
381        mLayoutManager.setItemMargin(margin);
382        requestLayout();
383    }
384
385    /**
386     * Set the margin in pixels between two child items vertically.
387     */
388    public void setVerticalMargin(int margin) {
389        mLayoutManager.setVerticalMargin(margin);
390        requestLayout();
391    }
392
393    /**
394     * Get the margin in pixels between two child items vertically.
395     */
396    public int getVerticalMargin() {
397        return mLayoutManager.getVerticalMargin();
398    }
399
400    /**
401     * Set the margin in pixels between two child items horizontally.
402     */
403    public void setHorizontalMargin(int margin) {
404        mLayoutManager.setHorizontalMargin(margin);
405        requestLayout();
406    }
407
408    /**
409     * Get the margin in pixels between two child items horizontally.
410     */
411    public int getHorizontalMargin() {
412        return mLayoutManager.getHorizontalMargin();
413    }
414
415    /**
416     * Register a callback to be invoked when an item in BaseGridView has
417     * been selected.  Note that the listener may be invoked when there is a
418     * layout pending on the view, affording the listener an opportunity to
419     * adjust the upcoming layout based on the selection state.
420     *
421     * @param listener The listener to be invoked.
422     */
423    public void setOnChildSelectedListener(OnChildSelectedListener listener) {
424        mLayoutManager.setOnChildSelectedListener(listener);
425    }
426
427    /**
428     * Change the selected item immediately without animation.
429     */
430    public void setSelectedPosition(int position) {
431        mLayoutManager.setSelection(this, position);
432    }
433
434    /**
435     * Change the selected item and run an animation to scroll to the target
436     * position.
437     */
438    public void setSelectedPositionSmooth(int position) {
439        mLayoutManager.setSelectionSmooth(this, position);
440    }
441
442    /**
443     * Get the selected item position.
444     */
445    public int getSelectedPosition() {
446        return mLayoutManager.getSelection();
447    }
448
449    /**
450     * Set if an animation should run when a child changes size or when adding
451     * or removing a child.
452     * <p><i>Unstable API, might change later.</i>
453     */
454    public void setAnimateChildLayout(boolean animateChildLayout) {
455        if (mAnimateChildLayout != animateChildLayout) {
456            mAnimateChildLayout = animateChildLayout;
457            if (!mAnimateChildLayout) {
458                mSavedItemAnimator = getItemAnimator();
459                super.setItemAnimator(null);
460            } else {
461                super.setItemAnimator(mSavedItemAnimator);
462            }
463        }
464    }
465
466    /**
467     * Return true if an animation will run when a child changes size or when
468     * adding or removing a child.
469     * <p><i>Unstable API, might change later.</i>
470     */
471    public boolean isChildLayoutAnimated() {
472        return mAnimateChildLayout;
473    }
474
475    /**
476     * Describes how the child views are positioned. Defaults to
477     * GRAVITY_TOP|GRAVITY_LEFT.
478     *
479     * @param gravity See {@link android.view.Gravity}
480     */
481    public void setGravity(int gravity) {
482        mLayoutManager.setGravity(gravity);
483        requestLayout();
484    }
485
486    @Override
487    public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
488        return mLayoutManager.gridOnRequestFocusInDescendants(this, direction,
489                previouslyFocusedRect);
490    }
491
492    /**
493     * Get the x/y offsets to final position from current position if the view
494     * is selected.
495     *
496     * @param view The view to get offsets.
497     * @param offsets offsets[0] holds offset of X, offsets[1] holds offset of
498     *        Y.
499     */
500    public void getViewSelectedOffsets(View view, int[] offsets) {
501        mLayoutManager.getViewSelectedOffsets(view, offsets);
502    }
503
504    @Override
505    public int getChildDrawingOrder(int childCount, int i) {
506        return mLayoutManager.getChildDrawingOrder(this, childCount, i);
507    }
508
509    final boolean isChildrenDrawingOrderEnabledInternal() {
510        return isChildrenDrawingOrderEnabled();
511    }
512
513    /**
514     * Disable or enable focus search.
515     */
516    public final void setFocusSearchDisabled(boolean disabled) {
517        mLayoutManager.setFocusSearchDisabled(disabled);
518    }
519
520    /**
521     * Return true if focus search is disabled.
522     */
523    public final boolean isFocusSearchDisabled() {
524        return mLayoutManager.isFocusSearchDisabled();
525    }
526
527    /**
528     * Enable or disable layout.  All children will be removed when layout is
529     * disabled.
530     */
531    public void setLayoutEnabled(boolean layoutEnabled) {
532        mLayoutManager.setLayoutEnabled(layoutEnabled);
533    }
534
535    /**
536     * Change and override children's visibility.
537     */
538    public void setChildrenVisibility(int visibility) {
539        mLayoutManager.setChildrenVisibility(visibility);
540    }
541
542    /**
543     * Enable or disable pruning child.  Disable is useful during transition.
544     */
545    public void setPruneChild(boolean pruneChild) {
546        mLayoutManager.setPruneChild(pruneChild);
547    }
548
549    /**
550     * Enable or disable scrolling.  Disable is useful during transition.
551     */
552    public void setScrollEnabled(boolean scrollEnabled) {
553        mLayoutManager.setScrollEnabled(scrollEnabled);
554    }
555
556    /**
557     * Returns true if scrolling is enabled.
558     */
559    public boolean isScrollEnabled() {
560        return mLayoutManager.isScrollEnabled();
561    }
562
563    /**
564     * Returns true if the view at the given position has a same row sibling
565     * in front of it.
566     *
567     * @param position Position in adapter.
568     */
569    public boolean hasPreviousViewInSameRow(int position) {
570        return mLayoutManager.hasPreviousViewInSameRow(position);
571    }
572
573    /**
574     * Enable or disable the default "focus draw at last" order rule.
575     */
576    public void setFocusDrawingOrderEnabled(boolean enabled) {
577        super.setChildrenDrawingOrderEnabled(enabled);
578    }
579
580    /**
581     * Returns true if default "focus draw at last" order rule is enabled.
582     */
583    public boolean isFocusDrawingOrderEnabled() {
584        return super.isChildrenDrawingOrderEnabled();
585    }
586
587    /**
588     * Sets the touch intercept listener.
589     */
590    public void setOnTouchInterceptListener(OnTouchInterceptListener listener) {
591        mOnTouchInterceptListener = listener;
592    }
593
594    /**
595     * Sets the generic motion intercept listener.
596     */
597    public void setOnMotionInterceptListener(OnMotionInterceptListener listener) {
598        mOnMotionInterceptListener = listener;
599    }
600
601    /**
602     * Sets the key intercept listener.
603     */
604    public void setOnKeyInterceptListener(OnKeyInterceptListener listener) {
605        mOnKeyInterceptListener = listener;
606    }
607
608    @Override
609    public boolean dispatchKeyEvent(KeyEvent event) {
610        if (mOnKeyInterceptListener != null) {
611            if (mOnKeyInterceptListener.onInterceptKeyEvent(event)) {
612                return true;
613            }
614        }
615        return super.dispatchKeyEvent(event);
616    }
617
618    @Override
619    public boolean dispatchTouchEvent(MotionEvent event) {
620        if (mOnTouchInterceptListener != null) {
621            if (mOnTouchInterceptListener.onInterceptTouchEvent(event)) {
622                return true;
623            }
624        }
625        return super.dispatchTouchEvent(event);
626    }
627
628    @Override
629    public boolean dispatchGenericFocusedEvent(MotionEvent event) {
630        if (mOnMotionInterceptListener != null) {
631            if (mOnMotionInterceptListener.onInterceptMotionEvent(event)) {
632                return true;
633            }
634        }
635        return super.dispatchGenericFocusedEvent(event);
636    }
637
638    /**
639     * @return policy for saving children.  One of {@link #SAVE_NO_CHILD}
640     * {@link #SAVE_ON_SCREEN_CHILD} {@link #SAVE_LIMITED_CHILD} {@link #SAVE_ALL_CHILD}.
641     */
642    public final int getSaveChildrenPolicy() {
643        return mLayoutManager.mChildrenStates.getSavePolicy();
644    }
645
646    /**
647     * @return The limit number when {@link #getSaveChildrenPolicy()} is
648     *         {@link #SAVE_LIMITED_CHILD}
649     */
650    public final int getSaveChildrenLimitNumber() {
651        return mLayoutManager.mChildrenStates.getLimitNumber();
652    }
653
654    /**
655     * Set policy for saving children.
656     * @param savePolicy One of {@link #SAVE_NO_CHILD} {@link #SAVE_ON_SCREEN_CHILD}
657     * {@link #SAVE_LIMITED_CHILD} {@link #SAVE_ALL_CHILD}.
658     */
659    public final void setSaveChildrenPolicy(int savePolicy) {
660        mLayoutManager.mChildrenStates.setSavePolicy(savePolicy);
661    }
662
663    /**
664     * Set limit number when {@link #getSaveChildrenPolicy()} is {@link #SAVE_LIMITED_CHILD}.
665     */
666    public final void setSaveChildrenLimitNumber(int limitNumber) {
667        mLayoutManager.mChildrenStates.setLimitNumber(limitNumber);
668    }
669
670    /**
671     * Set the factor by which children should be laid out beyond the view bounds
672     * in the direction of orientation.  1.0 disables over reach.
673     *
674     * @param fraction fraction of over reach
675     */
676    public final void setPrimaryOverReach(float fraction) {
677        mLayoutManager.setPrimaryOverReach(fraction);
678    }
679}
680