blob: 94eee280dd02995004d1de91caf5798b57b2d917 [file] [log] [blame]
/*
* 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;
/**
* Base class for vertically and horizontally scrolling lists. The items come
* from the {@link RecyclerView.Adapter} associated with this view.
* @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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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 = -1;
/**
* 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);
}
protected 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;
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);
}
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);
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();
}
/**
* Set the strategy used to scroll in response to item focus changing:
* <ul>
* <li>{@link #FOCUS_SCROLL_ALIGNED} (default) </li>
* <li>{@link #FOCUS_SCROLL_ITEM}</li>
* <li>{@link #FOCUS_SCROLL_PAGE}</li>
* </ul>
*/
public void setFocusScrollStrategy(int scrollStrategy) {
if (scrollStrategy != FOCUS_SCROLL_ALIGNED && scrollStrategy != FOCUS_SCROLL_ITEM
&& scrollStrategy != FOCUS_SCROLL_PAGE) {
throw new IllegalArgumentException("Invalid scrollStrategy");
}
mLayoutManager.setFocusScrollStrategy(scrollStrategy);
requestLayout();
}
/**
* Returns the strategy used to scroll in response to item focus changing.
* <ul>
* <li>{@link #FOCUS_SCROLL_ALIGNED} (default) </li>
* <li>{@link #FOCUS_SCROLL_ITEM}</li>
* <li>{@link #FOCUS_SCROLL_PAGE}</li>
* </ul>
*/
public int getFocusScrollStrategy() {
return mLayoutManager.getFocusScrollStrategy();
}
/**
* Set how the focused item gets aligned in the view.
*
* @param windowAlignment {@link #WINDOW_ALIGN_BOTH_EDGE},
* {@link #WINDOW_ALIGN_LOW_EDGE}, {@link #WINDOW_ALIGN_HIGH_EDGE} or
* {@link #WINDOW_ALIGN_NO_EDGE}.
*/
public void setWindowAlignment(int windowAlignment) {
mLayoutManager.setWindowAlignment(windowAlignment);
requestLayout();
}
/**
* Get how the focused item gets aligned in the view.
*
* @return {@link #WINDOW_ALIGN_BOTH_EDGE}, {@link #WINDOW_ALIGN_LOW_EDGE},
* {@link #WINDOW_ALIGN_HIGH_EDGE} or {@link #WINDOW_ALIGN_NO_EDGE}.
*/
public int getWindowAlignment() {
return mLayoutManager.getWindowAlignment();
}
/**
* Set the absolute offset in pixels for window alignment.
*
* @param offset The number of pixels to offset. Can be negative for
* alignment from the high edge, or positive for alignment from the
* low edge.
*/
public void setWindowAlignmentOffset(int offset) {
mLayoutManager.setWindowAlignmentOffset(offset);
requestLayout();
}
/**
* Get the absolute offset in pixels for window alignment.
*
* @return The number of pixels to offset. Will be negative for alignment
* from the high edge, or positive for alignment from the low edge.
* Default value is 0.
*/
public int getWindowAlignmentOffset() {
return mLayoutManager.getWindowAlignmentOffset();
}
/**
* Set offset percent for window alignment in addition to {@link
* #getWindowAlignmentOffset()}.
*
* @param offsetPercent Percentage to offset. E.g., 40 means 40% of the
* width from low edge. Use
* {@link #WINDOW_ALIGN_OFFSET_PERCENT_DISABLED} to disable.
*/
public void setWindowAlignmentOffsetPercent(float offsetPercent) {
mLayoutManager.setWindowAlignmentOffsetPercent(offsetPercent);
requestLayout();
}
/**
* Get offset percent for window alignment in addition to
* {@link #getWindowAlignmentOffset()}.
*
* @return Percentage to offset. E.g., 40 means 40% of the width from the
* low edge, or {@link #WINDOW_ALIGN_OFFSET_PERCENT_DISABLED} if
* disabled. Default value is 50.
*/
public float getWindowAlignmentOffsetPercent() {
return mLayoutManager.getWindowAlignmentOffsetPercent();
}
/**
* Set the absolute offset in pixels for item alignment.
*
* @param offset The number of pixels to offset. Can be negative for
* alignment from the high edge, or positive for alignment from the
* low edge.
*/
public void setItemAlignmentOffset(int offset) {
mLayoutManager.setItemAlignmentOffset(offset);
requestLayout();
}
/**
* Get the absolute offset in pixels for item alignment.
*
* @return The number of pixels to offset. Will be negative for alignment
* from the high edge, or positive for alignment from the low edge.
* Default value is 0.
*/
public int getItemAlignmentOffset() {
return mLayoutManager.getItemAlignmentOffset();
}
/**
* Set to true if include padding in calculating item align offset.
*
* @param withPadding When it is true: we include left/top padding for positive
* item offset, include right/bottom padding for negative item offset.
*/
public void setItemAlignmentOffsetWithPadding(boolean withPadding) {
mLayoutManager.setItemAlignmentOffsetWithPadding(withPadding);
requestLayout();
}
/**
* Returns true if include padding in calculating item align offset.
*/
public boolean isItemAlignmentOffsetWithPadding() {
return mLayoutManager.isItemAlignmentOffsetWithPadding();
}
/**
* Set offset percent for item alignment in addition to {@link
* #getItemAlignmentOffset()}.
*
* @param offsetPercent Percentage to offset. E.g., 40 means 40% of the
* width from the low edge. Use
* {@link #ITEM_ALIGN_OFFSET_PERCENT_DISABLED} to disable.
*/
public void setItemAlignmentOffsetPercent(float offsetPercent) {
mLayoutManager.setItemAlignmentOffsetPercent(offsetPercent);
requestLayout();
}
/**
* Get offset percent for item alignment in addition to {@link
* #getItemAlignmentOffset()}.
*
* @return Percentage to offset. E.g., 40 means 40% of the width from the
* low edge, or {@link #ITEM_ALIGN_OFFSET_PERCENT_DISABLED} if
* disabled. Default value is 50.
*/
public float getItemAlignmentOffsetPercent() {
return mLayoutManager.getItemAlignmentOffsetPercent();
}
/**
* Set the id of the view to align with. Use zero (default) for the item
* view itself.
*/
public void setItemAlignmentViewId(int viewId) {
mLayoutManager.setItemAlignmentViewId(viewId);
}
/**
* Get the id of the view to align with, or zero for the item view itself.
*/
public int getItemAlignmentViewId() {
return mLayoutManager.getItemAlignmentViewId();
}
/**
* Set the margin in pixels between two child items.
*/
public void setItemMargin(int margin) {
mLayoutManager.setItemMargin(margin);
requestLayout();
}
/**
* Set the margin in pixels between two child items vertically.
*/
public void setVerticalMargin(int margin) {
mLayoutManager.setVerticalMargin(margin);
requestLayout();
}
/**
* Get the margin in pixels between two child items vertically.
*/
public int getVerticalMargin() {
return mLayoutManager.getVerticalMargin();
}
/**
* Set the margin in pixels between two child items horizontally.
*/
public void setHorizontalMargin(int margin) {
mLayoutManager.setHorizontalMargin(margin);
requestLayout();
}
/**
* Get the margin in pixels between two child items horizontally.
*/
public int getHorizontalMargin() {
return mLayoutManager.getHorizontalMargin();
}
/**
* Register a callback to be invoked when an item in BaseGridView has
* been selected. Note that the listener may be invoked when there is a
* layout pending on the view, affording the listener an opportunity to
* adjust the upcoming layout based on the selection state.
*
* @param listener The listener to be invoked.
*/
public void setOnChildSelectedListener(OnChildSelectedListener listener) {
mLayoutManager.setOnChildSelectedListener(listener);
}
/**
* Change the selected item immediately without animation.
*/
public void setSelectedPosition(int position) {
mLayoutManager.setSelection(this, position);
}
/**
* Change the selected item and run an animation to scroll to the target
* position.
*/
public void setSelectedPositionSmooth(int position) {
mLayoutManager.setSelectionSmooth(this, position);
}
/**
* Get the selected item position.
*/
public int getSelectedPosition() {
return mLayoutManager.getSelection();
}
/**
* Set if an animation should run when a child changes size or when adding
* or removing a child.
* <p><i>Unstable API, might change later.</i>
*/
public void setAnimateChildLayout(boolean animateChildLayout) {
if (mAnimateChildLayout != animateChildLayout) {
mAnimateChildLayout = animateChildLayout;
if (!mAnimateChildLayout) {
mSavedItemAnimator = getItemAnimator();
super.setItemAnimator(null);
} else {
super.setItemAnimator(mSavedItemAnimator);
}
}
}
/**
* Return true if an animation will run when a child changes size or when
* adding or removing a child.
* <p><i>Unstable API, might change later.</i>
*/
public boolean isChildLayoutAnimated() {
return mAnimateChildLayout;
}
/**
* Describes how the child views are positioned. Defaults to
* GRAVITY_TOP|GRAVITY_LEFT.
*
* @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);
}
/**
* Get 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();
}
/**
* Disable or enable focus search.
*/
public final void setFocusSearchDisabled(boolean disabled) {
mLayoutManager.setFocusSearchDisabled(disabled);
}
/**
* Return true if focus search is disabled.
*/
public final boolean isFocusSearchDisabled() {
return mLayoutManager.isFocusSearchDisabled();
}
/**
* Enable or disable layout. All children will be removed when layout is
* disabled.
*/
public void setLayoutEnabled(boolean layoutEnabled) {
mLayoutManager.setLayoutEnabled(layoutEnabled);
}
/**
* Change and override children's visibility.
*/
public void setChildrenVisibility(int visibility) {
mLayoutManager.setChildrenVisibility(visibility);
}
/**
* Enable or disable pruning child. Disable is useful during transition.
*/
public void setPruneChild(boolean pruneChild) {
mLayoutManager.setPruneChild(pruneChild);
}
/**
* Enable or disable 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.
*
* @param position Position in adapter.
*/
public boolean hasPreviousViewInSameRow(int position) {
return mLayoutManager.hasPreviousViewInSameRow(position);
}
/**
* Enable or disable 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;
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (mOnKeyInterceptListener != null) {
if (mOnKeyInterceptListener.onInterceptKeyEvent(event)) {
return true;
}
}
return super.dispatchKeyEvent(event);
}
@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);
}
/**
* @return policy for saving children. 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();
}
/**
* @return The limit number when {@link #getSaveChildrenPolicy()} is
* {@link #SAVE_LIMITED_CHILD}
*/
public final int getSaveChildrenLimitNumber() {
return mLayoutManager.mChildrenStates.getLimitNumber();
}
/**
* Set 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);
}
/**
* Set limit number when {@link #getSaveChildrenPolicy()} is {@link #SAVE_LIMITED_CHILD}.
*/
public final void setSaveChildrenLimitNumber(int limitNumber) {
mLayoutManager.mChildrenStates.setLimitNumber(limitNumber);
}
/**
* Set the factor by which children should be laid out beyond the view bounds
* in the direction of orientation. 1.0 disables over reach.
*
* @param fraction fraction of over reach
*/
public final void setPrimaryOverReach(float fraction) {
mLayoutManager.setPrimaryOverReach(fraction);
}
@Override
public boolean hasOverlappingRendering() {
return mHasOverlappingRendering;
}
public void setHasOverlappingRendering(boolean hasOverlapping) {
mHasOverlappingRendering = hasOverlapping;
}
}