/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package android.support.v17.leanback.widget; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Rect; import android.support.v17.leanback.R; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.support.v7.widget.SimpleItemAnimator; /** * An abstract base class for vertically and horizontally scrolling lists. The items come * from the {@link RecyclerView.Adapter} associated with this view. * Do not directly use this class, use {@link VerticalGridView} and {@link HorizontalGridView}. * @hide */ abstract class BaseGridView extends RecyclerView { /** * Always keep focused item at a aligned position. Developer can use * WINDOW_ALIGN_XXX and ITEM_ALIGN_XXX to define how focused item is aligned. * In this mode, the last focused position will be remembered and restored when focus * is back to the view. */ public final static int FOCUS_SCROLL_ALIGNED = 0; /** * Scroll to make the focused item inside client area. */ public final static int FOCUS_SCROLL_ITEM = 1; /** * Scroll a page of items when focusing to item outside the client area. * The page size matches the client area size of RecyclerView. */ public final static int FOCUS_SCROLL_PAGE = 2; /** * The first item is aligned with the low edge of the viewport. When * navigating away from the first item, the focus maintains a middle * location. *
* For HorizontalGridView, low edge refers to left edge when RTL is false or * right edge when RTL is true. * For VerticalGridView, low edge refers to top edge. *
* The middle location is calculated by "windowAlignOffset" and * "windowAlignOffsetPercent"; if neither of these two is defined, the * default value is 1/2 of the size. */ public final static int WINDOW_ALIGN_LOW_EDGE = 1; /** * The last item is aligned with the high edge of the viewport when * navigating to the end of list. When navigating away from the end, the * focus maintains a middle location. *
* For HorizontalGridView, high edge refers to right edge when RTL is false or * left edge when RTL is true. * For VerticalGridView, high edge refers to bottom edge. *
* The middle location is calculated by "windowAlignOffset" and * "windowAlignOffsetPercent"; if neither of these two is defined, the * default value is 1/2 of the size. */ public final static int WINDOW_ALIGN_HIGH_EDGE = 1 << 1; /** * The first item and last item are aligned with the two edges of the * viewport. When navigating in the middle of list, the focus maintains a * middle location. *
* The middle location is calculated by "windowAlignOffset" and * "windowAlignOffsetPercent"; if neither of these two is defined, the * default value is 1/2 of the size. */ public final static int WINDOW_ALIGN_BOTH_EDGE = WINDOW_ALIGN_LOW_EDGE | WINDOW_ALIGN_HIGH_EDGE; /** * The focused item always stays in a middle location. *
* The middle location is calculated by "windowAlignOffset" and * "windowAlignOffsetPercent"; if neither of these two is defined, the * default value is 1/2 of the size. */ public final static int WINDOW_ALIGN_NO_EDGE = 0; /** * Value indicates that percent is not used. */ public final static float WINDOW_ALIGN_OFFSET_PERCENT_DISABLED = -1; /** * Value indicates that percent is not used. */ public final static float ITEM_ALIGN_OFFSET_PERCENT_DISABLED = ItemAlignmentFacet.ITEM_ALIGN_OFFSET_PERCENT_DISABLED; /** * Dont save states of any child views. */ public static final int SAVE_NO_CHILD = 0; /** * Only save on screen child views, the states are lost when they become off screen. */ public static final int SAVE_ON_SCREEN_CHILD = 1; /** * Save on screen views plus save off screen child views states up to * {@link #getSaveChildrenLimitNumber()}. */ public static final int SAVE_LIMITED_CHILD = 2; /** * Save on screen views plus save off screen child views without any limitation. * This might cause out of memory, only use it when you are dealing with limited data. */ public static final int SAVE_ALL_CHILD = 3; /** * Listener for intercepting touch dispatch events. */ public interface OnTouchInterceptListener { /** * Returns true if the touch dispatch event should be consumed. */ public boolean onInterceptTouchEvent(MotionEvent event); } /** * Listener for intercepting generic motion dispatch events. */ public interface OnMotionInterceptListener { /** * Returns true if the touch dispatch event should be consumed. */ public boolean onInterceptMotionEvent(MotionEvent event); } /** * Listener for intercepting key dispatch events. */ public interface OnKeyInterceptListener { /** * Returns true if the key dispatch event should be consumed. */ public boolean onInterceptKeyEvent(KeyEvent event); } public interface OnUnhandledKeyListener { /** * Returns true if the key event should be consumed. */ public boolean onUnhandledKey(KeyEvent event); } final GridLayoutManager mLayoutManager; /** * Animate layout changes from a child resizing or adding/removing a child. */ private boolean mAnimateChildLayout = true; private boolean mHasOverlappingRendering = true; private RecyclerView.ItemAnimator mSavedItemAnimator; private OnTouchInterceptListener mOnTouchInterceptListener; private OnMotionInterceptListener mOnMotionInterceptListener; private OnKeyInterceptListener mOnKeyInterceptListener; private RecyclerView.RecyclerListener mChainedRecyclerListener; private OnUnhandledKeyListener mOnUnhandledKeyListener; public BaseGridView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mLayoutManager = new GridLayoutManager(this); setLayoutManager(mLayoutManager); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); setHasFixedSize(true); setChildrenDrawingOrderEnabled(true); setWillNotDraw(true); setOverScrollMode(View.OVER_SCROLL_NEVER); // Disable change animation by default on leanback. // Change animation will create a new view and cause undesired // focus animation between the old view and new view. ((SimpleItemAnimator)getItemAnimator()).setSupportsChangeAnimations(false); super.setRecyclerListener(new RecyclerView.RecyclerListener() { @Override public void onViewRecycled(RecyclerView.ViewHolder holder) { mLayoutManager.onChildRecycled(holder); if (mChainedRecyclerListener != null) { mChainedRecyclerListener.onViewRecycled(holder); } } }); } protected void initBaseGridViewAttributes(Context context, AttributeSet attrs) { TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.lbBaseGridView); boolean throughFront = a.getBoolean(R.styleable.lbBaseGridView_focusOutFront, false); boolean throughEnd = a.getBoolean(R.styleable.lbBaseGridView_focusOutEnd, false); mLayoutManager.setFocusOutAllowed(throughFront, throughEnd); boolean throughSideStart = a.getBoolean(R.styleable.lbBaseGridView_focusOutSideStart, true); boolean throughSideEnd = a.getBoolean(R.styleable.lbBaseGridView_focusOutSideEnd, true); mLayoutManager.setFocusOutSideAllowed(throughSideStart, throughSideEnd); mLayoutManager.setVerticalMargin( a.getDimensionPixelSize(R.styleable.lbBaseGridView_verticalMargin, 0)); mLayoutManager.setHorizontalMargin( a.getDimensionPixelSize(R.styleable.lbBaseGridView_horizontalMargin, 0)); if (a.hasValue(R.styleable.lbBaseGridView_android_gravity)) { setGravity(a.getInt(R.styleable.lbBaseGridView_android_gravity, Gravity.NO_GRAVITY)); } a.recycle(); } /** * Sets the strategy used to scroll in response to item focus changing: *
Unstable API, might change later. */ public void setAnimateChildLayout(boolean animateChildLayout) { if (mAnimateChildLayout != animateChildLayout) { mAnimateChildLayout = animateChildLayout; if (!mAnimateChildLayout) { mSavedItemAnimator = getItemAnimator(); super.setItemAnimator(null); } else { super.setItemAnimator(mSavedItemAnimator); } } } /** * Returns true if an animation will run when a child changes size or when * adding or removing a child. *
Unstable API, might change later. */ public boolean isChildLayoutAnimated() { return mAnimateChildLayout; } /** * Sets the gravity used for child view positioning. Defaults to * GRAVITY_TOP|GRAVITY_START. * * @param gravity See {@link android.view.Gravity} */ public void setGravity(int gravity) { mLayoutManager.setGravity(gravity); requestLayout(); } @Override public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { return mLayoutManager.gridOnRequestFocusInDescendants(this, direction, previouslyFocusedRect); } /** * Returns the x/y offsets to final position from current position if the view * is selected. * * @param view The view to get offsets. * @param offsets offsets[0] holds offset of X, offsets[1] holds offset of Y. */ public void getViewSelectedOffsets(View view, int[] offsets) { mLayoutManager.getViewSelectedOffsets(view, offsets); } @Override public int getChildDrawingOrder(int childCount, int i) { return mLayoutManager.getChildDrawingOrder(this, childCount, i); } final boolean isChildrenDrawingOrderEnabledInternal() { return isChildrenDrawingOrderEnabled(); } @Override public View focusSearch(int direction) { if (isFocused()) { // focusSearch(int) is called when GridView itself is focused. // Calling focusSearch(view, int) to get next sibling of current selected child. View view = mLayoutManager.findViewByPosition(mLayoutManager.getSelection()); if (view != null) { return focusSearch(view, direction); } } // otherwise, go to mParent to perform focusSearch return super.focusSearch(direction); } @Override protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); mLayoutManager.onFocusChanged(gainFocus, direction, previouslyFocusedRect); } /** * Disables or enables focus search. */ public final void setFocusSearchDisabled(boolean disabled) { // LayoutManager may detachView and attachView in fastRelayout, it causes RowsFragment // re-gain focus after a BACK key pressed, so block children focus during transition. setDescendantFocusability(disabled ? FOCUS_BLOCK_DESCENDANTS: FOCUS_AFTER_DESCENDANTS); mLayoutManager.setFocusSearchDisabled(disabled); } /** * Returns true if focus search is disabled. */ public final boolean isFocusSearchDisabled() { return mLayoutManager.isFocusSearchDisabled(); } /** * Enables or disables layout. All children will be removed when layout is * disabled. */ public void setLayoutEnabled(boolean layoutEnabled) { mLayoutManager.setLayoutEnabled(layoutEnabled); } /** * Changes and overrides children's visibility. */ public void setChildrenVisibility(int visibility) { mLayoutManager.setChildrenVisibility(visibility); } /** * Enables or disables pruning of children. Disable is useful during transition. */ public void setPruneChild(boolean pruneChild) { mLayoutManager.setPruneChild(pruneChild); } /** * Enables or disables scrolling. Disable is useful during transition. */ public void setScrollEnabled(boolean scrollEnabled) { mLayoutManager.setScrollEnabled(scrollEnabled); } /** * Returns true if scrolling is enabled. */ public boolean isScrollEnabled() { return mLayoutManager.isScrollEnabled(); } /** * Returns true if the view at the given position has a same row sibling * in front of it. This will return true if first item view is not created. * So application should check in both {@link OnChildSelectedListener} and {@link * OnChildLaidOutListener}. * * @param position Position in adapter. */ public boolean hasPreviousViewInSameRow(int position) { return mLayoutManager.hasPreviousViewInSameRow(position); } /** * Enables or disables the default "focus draw at last" order rule. */ public void setFocusDrawingOrderEnabled(boolean enabled) { super.setChildrenDrawingOrderEnabled(enabled); } /** * Returns true if default "focus draw at last" order rule is enabled. */ public boolean isFocusDrawingOrderEnabled() { return super.isChildrenDrawingOrderEnabled(); } /** * Sets the touch intercept listener. */ public void setOnTouchInterceptListener(OnTouchInterceptListener listener) { mOnTouchInterceptListener = listener; } /** * Sets the generic motion intercept listener. */ public void setOnMotionInterceptListener(OnMotionInterceptListener listener) { mOnMotionInterceptListener = listener; } /** * Sets the key intercept listener. */ public void setOnKeyInterceptListener(OnKeyInterceptListener listener) { mOnKeyInterceptListener = listener; } /** * Sets the unhandled key listener. */ public void setOnUnhandledKeyListener(OnUnhandledKeyListener listener) { mOnUnhandledKeyListener = listener; } /** * Returns the unhandled key listener. */ public OnUnhandledKeyListener getOnUnhandledKeyListener() { return mOnUnhandledKeyListener; } @Override public boolean dispatchKeyEvent(KeyEvent event) { if (mOnKeyInterceptListener != null && mOnKeyInterceptListener.onInterceptKeyEvent(event)) { return true; } if (super.dispatchKeyEvent(event)) { return true; } if (mOnUnhandledKeyListener != null && mOnUnhandledKeyListener.onUnhandledKey(event)) { return true; } return false; } @Override public boolean dispatchTouchEvent(MotionEvent event) { if (mOnTouchInterceptListener != null) { if (mOnTouchInterceptListener.onInterceptTouchEvent(event)) { return true; } } return super.dispatchTouchEvent(event); } @Override public boolean dispatchGenericFocusedEvent(MotionEvent event) { if (mOnMotionInterceptListener != null) { if (mOnMotionInterceptListener.onInterceptMotionEvent(event)) { return true; } } return super.dispatchGenericFocusedEvent(event); } /** * Returns the policy for saving children. * * @return policy, one of {@link #SAVE_NO_CHILD} * {@link #SAVE_ON_SCREEN_CHILD} {@link #SAVE_LIMITED_CHILD} {@link #SAVE_ALL_CHILD}. */ public final int getSaveChildrenPolicy() { return mLayoutManager.mChildrenStates.getSavePolicy(); } /** * Returns the limit used when when {@link #getSaveChildrenPolicy()} is * {@link #SAVE_LIMITED_CHILD} */ public final int getSaveChildrenLimitNumber() { return mLayoutManager.mChildrenStates.getLimitNumber(); } /** * Sets the policy for saving children. * @param savePolicy One of {@link #SAVE_NO_CHILD} {@link #SAVE_ON_SCREEN_CHILD} * {@link #SAVE_LIMITED_CHILD} {@link #SAVE_ALL_CHILD}. */ public final void setSaveChildrenPolicy(int savePolicy) { mLayoutManager.mChildrenStates.setSavePolicy(savePolicy); } /** * Sets the limit number when {@link #getSaveChildrenPolicy()} is {@link #SAVE_LIMITED_CHILD}. */ public final void setSaveChildrenLimitNumber(int limitNumber) { mLayoutManager.mChildrenStates.setLimitNumber(limitNumber); } @Override public boolean hasOverlappingRendering() { return mHasOverlappingRendering; } public void setHasOverlappingRendering(boolean hasOverlapping) { mHasOverlappingRendering = hasOverlapping; } /** * Notify layout manager that layout directionality has been updated */ @Override public void onRtlPropertiesChanged(int layoutDirection) { mLayoutManager.onRtlPropertiesChanged(layoutDirection); } @Override public void setRecyclerListener(RecyclerView.RecyclerListener listener) { mChainedRecyclerListener = listener; } /** * Sets pixels of extra space for layout child in invisible area. * * @param extraLayoutSpace Pixels of extra space for layout invisible child. * Must be bigger or equals to 0. * @hide */ public void setExtraLayoutSpace(int extraLayoutSpace) { mLayoutManager.setExtraLayoutSpace(extraLayoutSpace); } /** * Returns pixels of extra space for layout child in invisible area. * * @hide */ public int getExtraLayoutSpace() { return mLayoutManager.getExtraLayoutSpace(); } }