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