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