/* * Copyright (C) 2015 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.animation.Animator; import android.animation.AnimatorInflater; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.os.Build.VERSION; import android.support.annotation.NonNull; import android.support.annotation.RestrictTo; import android.support.v17.leanback.R; import android.support.v17.leanback.transition.TransitionHelper; import android.support.v17.leanback.transition.TransitionListener; import android.support.v17.leanback.widget.GuidedActionAdapter.EditListener; import android.support.v17.leanback.widget.picker.DatePicker; import android.support.v4.content.ContextCompat; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; import android.util.TypedValue; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.View.AccessibilityDelegate; import android.view.ViewGroup; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.inputmethod.EditorInfo; import android.widget.Checkable; import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; import java.util.Calendar; import java.util.Collections; import java.util.List; import static android.support.annotation.RestrictTo.Scope.GROUP_ID; import static android.support.v17.leanback.widget.GuidedAction.EDITING_ACTIVATOR_VIEW; import static android.support.v17.leanback.widget.GuidedAction.EDITING_DESCRIPTION; import static android.support.v17.leanback.widget.GuidedAction.EDITING_NONE; import static android.support.v17.leanback.widget.GuidedAction.EDITING_TITLE; /** * GuidedActionsStylist is used within a {@link android.support.v17.leanback.app.GuidedStepFragment} * to supply the right-side panel where users can take actions. It consists of a container for the * list of actions, and a stationary selector view that indicates visually the location of focus. * GuidedActionsStylist has two different layouts: default is for normal actions including text, * radio, checkbox, DatePicker, etc, the other when {@link #setAsButtonActions()} is called is * recommended for button actions such as "yes", "no". *
* Many aspects of the base GuidedActionsStylist can be customized through theming; see the * theme attributes below. Note that these attributes are not set on individual elements in layout * XML, but instead would be set in a custom theme. See * Styles and Themes * for more information. *
* If these hooks are insufficient, this class may also be subclassed. Subclasses may wish to * override the {@link #onProvideLayoutId} method to change the layout used to display the * list container and selector; override {@link #onProvideItemLayoutId(int)} and * {@link #getItemViewType(GuidedAction)} method to change the layout used to display each action. *
* To support a "click to activate" view similar to DatePicker, app needs: *
* Note: If an alternate list layout is provided, the following view IDs must be supplied: *
* These view IDs must be present in order for the stylist to function. The list ID must correspond * to a {@link VerticalGridView} or subclass. *
* If an alternate item layout is provided, the following view IDs should be used to refer to base * elements: *
* These view IDs are allowed to be missing, in which case the corresponding views in {@link * GuidedActionsStylist.ViewHolder} will be null. *
* In order to support editable actions, the view associated with guidedactions_item_title should * be a subclass of {@link android.widget.EditText}, and should satisfy the {@link * ImeKeyMonitor} interface. * * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepImeAppearingAnimation * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepImeDisappearingAnimation * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsSelectorDrawable * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsListStyle * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedSubActionsListStyle * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedButtonActionsListStyle * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemContainerStyle * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemCheckmarkStyle * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemIconStyle * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemContentStyle * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemTitleStyle * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemDescriptionStyle * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemChevronStyle * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionPressedAnimation * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionUnpressedAnimation * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionEnabledChevronAlpha * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionDisabledChevronAlpha * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionTitleMinLines * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionTitleMaxLines * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionDescriptionMinLines * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionVerticalPadding * @see android.R.styleable#Theme_listChoiceIndicatorSingle * @see android.R.styleable#Theme_listChoiceIndicatorMultiple * @see android.support.v17.leanback.app.GuidedStepFragment * @see GuidedAction */ public class GuidedActionsStylist implements FragmentAnimationProvider { /** * Default viewType that associated with default layout Id for the action item. * @see #getItemViewType(GuidedAction) * @see #onProvideItemLayoutId(int) * @see #onCreateViewHolder(ViewGroup, int) */ public static final int VIEW_TYPE_DEFAULT = 0; /** * ViewType for DatePicker. */ public static final int VIEW_TYPE_DATE_PICKER = 1; final static ItemAlignmentFacet sGuidedActionItemAlignFacet; static { sGuidedActionItemAlignFacet = new ItemAlignmentFacet(); ItemAlignmentFacet.ItemAlignmentDef alignedDef = new ItemAlignmentFacet.ItemAlignmentDef(); alignedDef.setItemAlignmentViewId(R.id.guidedactions_item_title); alignedDef.setAlignedToTextViewBaseline(true); alignedDef.setItemAlignmentOffset(0); alignedDef.setItemAlignmentOffsetWithPadding(true); alignedDef.setItemAlignmentOffsetPercent(0); sGuidedActionItemAlignFacet.setAlignmentDefs(new ItemAlignmentFacet.ItemAlignmentDef[]{alignedDef}); } /** * ViewHolder caches information about the action item layouts' subviews. Subclasses of {@link * GuidedActionsStylist} may also wish to subclass this in order to add fields. * @see GuidedAction */ public static class ViewHolder extends RecyclerView.ViewHolder implements FacetProvider { GuidedAction mAction; private View mContentView; TextView mTitleView; TextView mDescriptionView; View mActivatorView; ImageView mIconView; ImageView mCheckmarkView; ImageView mChevronView; int mEditingMode = EDITING_NONE; private final boolean mIsSubAction; final AccessibilityDelegate mDelegate = new AccessibilityDelegate() { @Override public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { super.onInitializeAccessibilityEvent(host, event); event.setChecked(mAction != null && mAction.isChecked()); } @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(host, info); info.setCheckable( mAction != null && mAction.getCheckSetId() != GuidedAction.NO_CHECK_SET); info.setChecked(mAction != null && mAction.isChecked()); } }; /** * Constructs an ViewHolder and caches the relevant subviews. */ public ViewHolder(View v) { this(v, false); } /** * Constructs an ViewHolder for sub action and caches the relevant subviews. */ public ViewHolder(View v, boolean isSubAction) { super(v); mContentView = v.findViewById(R.id.guidedactions_item_content); mTitleView = (TextView) v.findViewById(R.id.guidedactions_item_title); mActivatorView = v.findViewById(R.id.guidedactions_activator_item); mDescriptionView = (TextView) v.findViewById(R.id.guidedactions_item_description); mIconView = (ImageView) v.findViewById(R.id.guidedactions_item_icon); mCheckmarkView = (ImageView) v.findViewById(R.id.guidedactions_item_checkmark); mChevronView = (ImageView) v.findViewById(R.id.guidedactions_item_chevron); mIsSubAction = isSubAction; v.setAccessibilityDelegate(mDelegate); } /** * Returns the content view within this view holder's view, where title and description are * shown. */ public View getContentView() { return mContentView; } /** * Returns the title view within this view holder's view. */ public TextView getTitleView() { return mTitleView; } /** * Convenience method to return an editable version of the title, if possible, * or null if the title view isn't an EditText. */ public EditText getEditableTitleView() { return (mTitleView instanceof EditText) ? (EditText)mTitleView : null; } /** * Returns the description view within this view holder's view. */ public TextView getDescriptionView() { return mDescriptionView; } /** * Convenience method to return an editable version of the description, if possible, * or null if the description view isn't an EditText. */ public EditText getEditableDescriptionView() { return (mDescriptionView instanceof EditText) ? (EditText)mDescriptionView : null; } /** * Returns the icon view within this view holder's view. */ public ImageView getIconView() { return mIconView; } /** * Returns the checkmark view within this view holder's view. */ public ImageView getCheckmarkView() { return mCheckmarkView; } /** * Returns the chevron view within this view holder's view. */ public ImageView getChevronView() { return mChevronView; } /** * Returns true if in editing title, description, or activator View, false otherwise. */ public boolean isInEditing() { return mEditingMode != EDITING_NONE; } /** * Returns true if in editing title, description, so IME would be open. * @return True if in editing title, description, so IME would be open, false otherwise. */ public boolean isInEditingText() { return mEditingMode == EDITING_TITLE || mEditingMode == EDITING_DESCRIPTION; } /** * Returns true if the TextView is in editing title, false otherwise. */ public boolean isInEditingTitle() { return mEditingMode == EDITING_TITLE; } /** * Returns true if the TextView is in editing description, false otherwise. */ public boolean isInEditingDescription() { return mEditingMode == EDITING_DESCRIPTION; } /** * Returns true if is in editing activator view with id guidedactions_activator_item, false * otherwise. */ public boolean isInEditingActivatorView() { return mEditingMode == EDITING_ACTIVATOR_VIEW; } /** * @return Current editing title view or description view or activator view or null if not * in editing. */ public View getEditingView() { switch(mEditingMode) { case EDITING_TITLE: return mTitleView; case EDITING_DESCRIPTION: return mDescriptionView; case EDITING_ACTIVATOR_VIEW: return mActivatorView; case EDITING_NONE: default: return null; } } /** * @return True if bound action is inside {@link GuidedAction#getSubActions()}, false * otherwise. */ public boolean isSubAction() { return mIsSubAction; } /** * @return Currently bound action. */ public GuidedAction getAction() { return mAction; } void setActivated(boolean activated) { mActivatorView.setActivated(activated); if (itemView instanceof GuidedActionItemContainer) { ((GuidedActionItemContainer) itemView).setFocusOutAllowed(!activated); } } @Override public Object getFacet(Class> facetClass) { if (facetClass == ItemAlignmentFacet.class) { return sGuidedActionItemAlignFacet; } return null; } } private static String TAG = "GuidedActionsStylist"; ViewGroup mMainView; private VerticalGridView mActionsGridView; VerticalGridView mSubActionsGridView; private View mSubActionsBackground; private View mBgView; private View mContentView; private boolean mButtonActions; // Cached values from resources private float mEnabledTextAlpha; private float mDisabledTextAlpha; private float mEnabledDescriptionAlpha; private float mDisabledDescriptionAlpha; private float mEnabledChevronAlpha; private float mDisabledChevronAlpha; private int mTitleMinLines; private int mTitleMaxLines; private int mDescriptionMinLines; private int mVerticalPadding; private int mDisplayHeight; private EditListener mEditListener; private GuidedAction mExpandedAction = null; Object mExpandTransition; /** * Creates a view appropriate for displaying a list of GuidedActions, using the provided * inflater and container. *
* Note: Does not actually add the created view to the container; the caller should do
* this.
* @param inflater The layout inflater to be used when constructing the view.
* @param container The view group to be passed in the call to
* LayoutInflater.inflate
.
* @return The view to be added to the caller's view hierarchy.
*/
public View onCreateView(LayoutInflater inflater, final ViewGroup container) {
TypedArray ta = inflater.getContext().getTheme().obtainStyledAttributes(
R.styleable.LeanbackGuidedStepTheme);
float keylinePercent = ta.getFloat(R.styleable.LeanbackGuidedStepTheme_guidedStepKeyline,
40);
mMainView = (ViewGroup) inflater.inflate(onProvideLayoutId(), container, false);
mContentView = mMainView.findViewById(mButtonActions ? R.id.guidedactions_content2 :
R.id.guidedactions_content);
mBgView = mMainView.findViewById(mButtonActions ? R.id.guidedactions_list_background2 :
R.id.guidedactions_list_background);
if (mMainView instanceof VerticalGridView) {
mActionsGridView = (VerticalGridView) mMainView;
} else {
mActionsGridView = (VerticalGridView) mMainView.findViewById(mButtonActions ?
R.id.guidedactions_list2 : R.id.guidedactions_list);
if (mActionsGridView == null) {
throw new IllegalStateException("No ListView exists.");
}
mActionsGridView.setWindowAlignmentOffsetPercent(keylinePercent);
mActionsGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
if (!mButtonActions) {
mSubActionsGridView = (VerticalGridView) mMainView.findViewById(
R.id.guidedactions_sub_list);
mSubActionsBackground = mMainView.findViewById(
R.id.guidedactions_sub_list_background);
}
}
mActionsGridView.setFocusable(false);
mActionsGridView.setFocusableInTouchMode(false);
// Cache widths, chevron alpha values, max and min text lines, etc
Context ctx = mMainView.getContext();
TypedValue val = new TypedValue();
mEnabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionEnabledChevronAlpha);
mDisabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionDisabledChevronAlpha);
mTitleMinLines = getInteger(ctx, val, R.attr.guidedActionTitleMinLines);
mTitleMaxLines = getInteger(ctx, val, R.attr.guidedActionTitleMaxLines);
mDescriptionMinLines = getInteger(ctx, val, R.attr.guidedActionDescriptionMinLines);
mVerticalPadding = getDimension(ctx, val, R.attr.guidedActionVerticalPadding);
mDisplayHeight = ((WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay().getHeight();
mEnabledTextAlpha = Float.valueOf(ctx.getResources().getString(R.string
.lb_guidedactions_item_unselected_text_alpha));
mDisabledTextAlpha = Float.valueOf(ctx.getResources().getString(R.string
.lb_guidedactions_item_disabled_text_alpha));
mEnabledDescriptionAlpha = Float.valueOf(ctx.getResources().getString(R.string
.lb_guidedactions_item_unselected_description_text_alpha));
mDisabledDescriptionAlpha = Float.valueOf(ctx.getResources().getString(R.string
.lb_guidedactions_item_disabled_description_text_alpha));
return mMainView;
}
/**
* Choose the layout resource for button actions in {@link #onProvideLayoutId()}.
*/
public void setAsButtonActions() {
if (mMainView != null) {
throw new IllegalStateException("setAsButtonActions() must be called before creating "
+ "views");
}
mButtonActions = true;
}
/**
* Returns true if it is button actions list, false for normal actions list.
* @return True if it is button actions list, false for normal actions list.
*/
public boolean isButtonActions() {
return mButtonActions;
}
/**
* Called when destroy the View created by GuidedActionsStylist.
*/
public void onDestroyView() {
mExpandedAction = null;
mExpandTransition = null;
mActionsGridView = null;
mSubActionsGridView = null;
mSubActionsBackground = null;
mContentView = null;
mBgView = null;
mMainView = null;
}
/**
* Returns the VerticalGridView that displays the list of GuidedActions.
* @return The VerticalGridView for this presenter.
*/
public VerticalGridView getActionsGridView() {
return mActionsGridView;
}
/**
* Returns the VerticalGridView that displays the sub actions list of an expanded action.
* @return The VerticalGridView that displays the sub actions list of an expanded action.
*/
public VerticalGridView getSubActionsGridView() {
return mSubActionsGridView;
}
/**
* Provides the resource ID of the layout defining the host view for the list of guided actions.
* Subclasses may override to provide their own customized layouts. The base implementation
* returns {@link android.support.v17.leanback.R.layout#lb_guidedactions} or
* {@link android.support.v17.leanback.R.layout#lb_guidedbuttonactions} if
* {@link #isButtonActions()} is true. If overridden, the substituted layout should contain
* matching IDs for any views that should be managed by the base class; this can be achieved by
* starting with a copy of the base layout file.
*
* @return The resource ID of the layout to be inflated to define the host view for the list of
* GuidedActions.
*/
public int onProvideLayoutId() {
return mButtonActions ? R.layout.lb_guidedbuttonactions : R.layout.lb_guidedactions;
}
/**
* Return view type of action, each different type can have differently associated layout Id.
* Default implementation returns {@link #VIEW_TYPE_DEFAULT}.
* @param action The action object.
* @return View type that used in {@link #onProvideItemLayoutId(int)}.
*/
public int getItemViewType(GuidedAction action) {
if (action instanceof GuidedDatePickerAction) {
return VIEW_TYPE_DATE_PICKER;
}
return VIEW_TYPE_DEFAULT;
}
/**
* Provides the resource ID of the layout defining the view for an individual guided actions.
* Subclasses may override to provide their own customized layouts. The base implementation
* returns {@link android.support.v17.leanback.R.layout#lb_guidedactions_item}. If overridden,
* the substituted layout should contain matching IDs for any views that should be managed by
* the base class; this can be achieved by starting with a copy of the base layout file. Note
* that in order for the item to support editing, the title view should both subclass {@link
* android.widget.EditText} and implement {@link ImeKeyMonitor}; see {@link
* GuidedActionEditText}. To support different types of Layouts, override {@link
* #onProvideItemLayoutId(int)}.
* @return The resource ID of the layout to be inflated to define the view to display an
* individual GuidedAction.
*/
public int onProvideItemLayoutId() {
return R.layout.lb_guidedactions_item;
}
/**
* Provides the resource ID of the layout defining the view for an individual guided actions.
* Subclasses may override to provide their own customized layouts. The base implementation
* supports:
*
* Note: Should not actually add the created view to the parent; the caller will do * this. * @param parent The view group to be used as the parent of the new view. * @return The view to be added to the caller's view hierarchy. */ public ViewHolder onCreateViewHolder(ViewGroup parent) { LayoutInflater inflater = LayoutInflater.from(parent.getContext()); View v = inflater.inflate(onProvideItemLayoutId(), parent, false); return new ViewHolder(v, parent == mSubActionsGridView); } /** * Constructs a {@link ViewHolder} capable of representing {@link GuidedAction}s. Subclasses * may choose to return a subclass of ViewHolder. *
* Note: Should not actually add the created view to the parent; the caller will do
* this.
* @param parent The view group to be used as the parent of the new view.
* @param viewType The viewType returned by {@link #getItemViewType(GuidedAction)}
* @return The view to be added to the caller's view hierarchy.
*/
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == VIEW_TYPE_DEFAULT) {
return onCreateViewHolder(parent);
}
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View v = inflater.inflate(onProvideItemLayoutId(viewType), parent, false);
return new ViewHolder(v, parent == mSubActionsGridView);
}
/**
* Binds a {@link ViewHolder} to a particular {@link GuidedAction}.
* @param vh The view holder to be associated with the given action.
* @param action The guided action to be displayed by the view holder's view.
* @return The view to be added to the caller's view hierarchy.
*/
public void onBindViewHolder(ViewHolder vh, GuidedAction action) {
vh.mAction = action;
if (vh.mTitleView != null) {
vh.mTitleView.setText(action.getTitle());
vh.mTitleView.setAlpha(action.isEnabled() ? mEnabledTextAlpha : mDisabledTextAlpha);
vh.mTitleView.setFocusable(false);
vh.mTitleView.setClickable(false);
vh.mTitleView.setLongClickable(false);
}
if (vh.mDescriptionView != null) {
vh.mDescriptionView.setText(action.getDescription());
vh.mDescriptionView.setVisibility(TextUtils.isEmpty(action.getDescription()) ?
View.GONE : View.VISIBLE);
vh.mDescriptionView.setAlpha(action.isEnabled() ? mEnabledDescriptionAlpha :
mDisabledDescriptionAlpha);
vh.mDescriptionView.setFocusable(false);
vh.mDescriptionView.setClickable(false);
vh.mDescriptionView.setLongClickable(false);
}
// Clients might want the check mark view to be gone entirely, in which case, ignore it.
if (vh.mCheckmarkView != null) {
onBindCheckMarkView(vh, action);
}
setIcon(vh.mIconView, action);
if (action.hasMultilineDescription()) {
if (vh.mTitleView != null) {
setMaxLines(vh.mTitleView, mTitleMaxLines);
if (vh.mDescriptionView != null) {
vh.mDescriptionView.setMaxHeight(getDescriptionMaxHeight(
vh.itemView.getContext(), vh.mTitleView));
}
}
} else {
if (vh.mTitleView != null) {
setMaxLines(vh.mTitleView, mTitleMinLines);
}
if (vh.mDescriptionView != null) {
setMaxLines(vh.mDescriptionView, mDescriptionMinLines);
}
}
if (vh.mActivatorView != null) {
onBindActivatorView(vh, action);
}
setEditingMode(vh, action, false);
if (action.isFocusable()) {
vh.itemView.setFocusable(true);
((ViewGroup) vh.itemView).setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
} else {
vh.itemView.setFocusable(false);
((ViewGroup) vh.itemView).setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
}
setupImeOptions(vh, action);
updateChevronAndVisibility(vh);
}
private static void setMaxLines(TextView view, int maxLines) {
// setSingleLine must be called before setMaxLines because it resets maximum to
// Integer.MAX_VALUE.
if (maxLines == 1) {
view.setSingleLine(true);
} else {
view.setSingleLine(false);
view.setMaxLines(maxLines);
}
}
/**
* Called by {@link #onBindViewHolder(ViewHolder, GuidedAction)} to setup IME options. Default
* implementation assigns {@link EditorInfo#IME_ACTION_DONE}. Subclass may override.
* @param vh The view holder to be associated with the given action.
* @param action The guided action to be displayed by the view holder's view.
*/
protected void setupImeOptions(ViewHolder vh, GuidedAction action) {
setupNextImeOptions(vh.getEditableTitleView());
setupNextImeOptions(vh.getEditableDescriptionView());
}
private void setupNextImeOptions(EditText edit) {
if (edit != null) {
edit.setImeOptions(EditorInfo.IME_ACTION_NEXT);
}
}
public void setEditingMode(ViewHolder vh, GuidedAction action, boolean editing) {
if (editing != vh.isInEditing() && !isInExpandTransition()) {
onEditingModeChange(vh, action, editing);
}
}
protected void onEditingModeChange(ViewHolder vh, GuidedAction action, boolean editing) {
action = vh.getAction();
TextView titleView = vh.getTitleView();
TextView descriptionView = vh.getDescriptionView();
if (editing) {
CharSequence editTitle = action.getEditTitle();
if (titleView != null && editTitle != null) {
titleView.setText(editTitle);
}
CharSequence editDescription = action.getEditDescription();
if (descriptionView != null && editDescription != null) {
descriptionView.setText(editDescription);
}
if (action.isDescriptionEditable()) {
if (descriptionView != null) {
descriptionView.setVisibility(View.VISIBLE);
descriptionView.setInputType(action.getDescriptionEditInputType());
}
vh.mEditingMode = EDITING_DESCRIPTION;
} else if (action.isEditable()){
if (titleView != null) {
titleView.setInputType(action.getEditInputType());
}
vh.mEditingMode = EDITING_TITLE;
} else if (vh.mActivatorView != null) {
onEditActivatorView(vh, action, editing);
vh.mEditingMode = EDITING_ACTIVATOR_VIEW;
}
} else {
if (titleView != null) {
titleView.setText(action.getTitle());
}
if (descriptionView != null) {
descriptionView.setText(action.getDescription());
}
if (vh.mEditingMode == EDITING_DESCRIPTION) {
if (descriptionView != null) {
descriptionView.setVisibility(TextUtils.isEmpty(action.getDescription()) ?
View.GONE : View.VISIBLE);
descriptionView.setInputType(action.getDescriptionInputType());
}
} else if (vh.mEditingMode == EDITING_TITLE) {
if (titleView != null) {
titleView.setInputType(action.getInputType());
}
} else if (vh.mEditingMode == EDITING_ACTIVATOR_VIEW) {
if (vh.mActivatorView != null) {
onEditActivatorView(vh, action, editing);
}
}
vh.mEditingMode = EDITING_NONE;
}
}
/**
* Animates the view holder's view (or subviews thereof) when the action has had its focus
* state changed.
* @param vh The view holder associated with the relevant action.
* @param focused True if the action has become focused, false if it has lost focus.
*/
public void onAnimateItemFocused(ViewHolder vh, boolean focused) {
// No animations for this, currently, because the animation is done on
// mSelectorView
}
/**
* Animates the view holder's view (or subviews thereof) when the action has had its press
* state changed.
* @param vh The view holder associated with the relevant action.
* @param pressed True if the action has been pressed, false if it has been unpressed.
*/
public void onAnimateItemPressed(ViewHolder vh, boolean pressed) {
int attr = pressed ? R.attr.guidedActionPressedAnimation :
R.attr.guidedActionUnpressedAnimation;
createAnimator(vh.itemView, attr).start();
}
/**
* Resets the view holder's view to unpressed state.
* @param vh The view holder associated with the relevant action.
*/
public void onAnimateItemPressedCancelled(ViewHolder vh) {
createAnimator(vh.itemView, R.attr.guidedActionUnpressedAnimation).end();
}
/**
* Animates the view holder's view (or subviews thereof) when the action has had its check state
* changed. Default implementation calls setChecked() if {@link ViewHolder#getCheckmarkView()}
* is instance of {@link Checkable}.
*
* @param vh The view holder associated with the relevant action.
* @param checked True if the action has become checked, false if it has become unchecked.
* @see #onBindCheckMarkView(ViewHolder, GuidedAction)
*/
public void onAnimateItemChecked(ViewHolder vh, boolean checked) {
if (vh.mCheckmarkView instanceof Checkable) {
((Checkable) vh.mCheckmarkView).setChecked(checked);
}
}
/**
* Sets states of check mark view, called by {@link #onBindViewHolder(ViewHolder, GuidedAction)}
* when action's checkset Id is other than {@link GuidedAction#NO_CHECK_SET}. Default
* implementation assigns drawable loaded from theme attribute
* {@link android.R.attr#listChoiceIndicatorMultiple} for checkbox or
* {@link android.R.attr#listChoiceIndicatorSingle} for radio button. Subclass rarely needs
* override the method, instead app can provide its own drawable that supports transition
* animations, change theme attributes {@link android.R.attr#listChoiceIndicatorMultiple} and
* {@link android.R.attr#listChoiceIndicatorSingle} in {android.support.v17.leanback.R.
* styleable#LeanbackGuidedStepTheme}.
*
* @param vh The view holder associated with the relevant action.
* @param action The GuidedAction object to bind to.
* @see #onAnimateItemChecked(ViewHolder, boolean)
*/
public void onBindCheckMarkView(ViewHolder vh, GuidedAction action) {
if (action.getCheckSetId() != GuidedAction.NO_CHECK_SET) {
vh.mCheckmarkView.setVisibility(View.VISIBLE);
int attrId = action.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID ?
android.R.attr.listChoiceIndicatorMultiple :
android.R.attr.listChoiceIndicatorSingle;
final Context context = vh.mCheckmarkView.getContext();
Drawable drawable = null;
TypedValue typedValue = new TypedValue();
if (context.getTheme().resolveAttribute(attrId, typedValue, true)) {
drawable = ContextCompat.getDrawable(context, typedValue.resourceId);
}
vh.mCheckmarkView.setImageDrawable(drawable);
if (vh.mCheckmarkView instanceof Checkable) {
((Checkable) vh.mCheckmarkView).setChecked(action.isChecked());
}
} else {
vh.mCheckmarkView.setVisibility(View.GONE);
}
}
/**
* Performs binding activator view value to action. Default implementation supports
* GuidedDatePickerAction, subclass may override to add support of other views.
* @param vh ViewHolder of activator view.
* @param action GuidedAction to bind.
*/
public void onBindActivatorView(ViewHolder vh, GuidedAction action) {
if (action instanceof GuidedDatePickerAction) {
GuidedDatePickerAction dateAction = (GuidedDatePickerAction) action;
DatePicker dateView = (DatePicker) vh.mActivatorView;
dateView.setDatePickerFormat(dateAction.getDatePickerFormat());
if (dateAction.getMinDate() != Long.MIN_VALUE) {
dateView.setMinDate(dateAction.getMinDate());
}
if (dateAction.getMaxDate() != Long.MAX_VALUE) {
dateView.setMaxDate(dateAction.getMaxDate());
}
Calendar c = Calendar.getInstance();
c.setTimeInMillis(dateAction.getDate());
dateView.updateDate(c.get(Calendar.YEAR), c.get(Calendar.MONTH),
c.get(Calendar.DAY_OF_MONTH), false);
}
}
/**
* Performs updating GuidedAction from activator view. Default implementation supports
* GuidedDatePickerAction, subclass may override to add support of other views.
* @param vh ViewHolder of activator view.
* @param action GuidedAction to update.
* @return True if value has been updated, false otherwise.
*/
public boolean onUpdateActivatorView(ViewHolder vh, GuidedAction action) {
if (action instanceof GuidedDatePickerAction) {
GuidedDatePickerAction dateAction = (GuidedDatePickerAction) action;
DatePicker dateView = (DatePicker) vh.mActivatorView;
if (dateAction.getDate() != dateView.getDate()) {
dateAction.setDate(dateView.getDate());
return true;
}
}
return false;
}
/**
* Sets listener for reporting view being edited.
* @hide
*/
@RestrictTo(GROUP_ID)
public void setEditListener(EditListener listener) {
mEditListener = listener;
}
void onEditActivatorView(final ViewHolder vh, final GuidedAction action,
boolean editing) {
if (editing) {
vh.itemView.setFocusable(false);
vh.mActivatorView.requestFocus();
setExpandedViewHolder(vh);
vh.mActivatorView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!isInExpandTransition()) {
setEditingMode(vh, action, false);
}
}
});
} else {
if (onUpdateActivatorView(vh, action)) {
if (mEditListener != null) {
mEditListener.onGuidedActionEditedAndProceed(action);
}
}
vh.itemView.setFocusable(true);
vh.itemView.requestFocus();
setExpandedViewHolder(null);
vh.mActivatorView.setOnClickListener(null);
vh.mActivatorView.setClickable(false);
}
}
/**
* Sets states of chevron view, called by {@link #onBindViewHolder(ViewHolder, GuidedAction)}.
* Subclass may override.
*
* @param vh The view holder associated with the relevant action.
* @param action The GuidedAction object to bind to.
*/
public void onBindChevronView(ViewHolder vh, GuidedAction action) {
final boolean hasNext = action.hasNext();
final boolean hasSubActions = action.hasSubActions();
if (hasNext || hasSubActions) {
vh.mChevronView.setVisibility(View.VISIBLE);
vh.mChevronView.setAlpha(action.isEnabled() ? mEnabledChevronAlpha :
mDisabledChevronAlpha);
if (hasNext) {
float r = mMainView != null
&& mMainView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? 180f : 0f;
vh.mChevronView.setRotation(r);
} else if (action == mExpandedAction) {
vh.mChevronView.setRotation(270);
} else {
vh.mChevronView.setRotation(90);
}
} else {
vh.mChevronView.setVisibility(View.GONE);
}
}
/**
* Expands or collapse the sub actions list view.
* @param avh When not null, fill sub actions list of this ViewHolder into sub actions list and
* hide the other items in main list. When null, collapse the sub actions list.
*/
public void setExpandedViewHolder(ViewHolder avh) {
if (isInExpandTransition()) {
return;
}
if (isExpandTransitionSupported()) {
startExpandedTransition(avh);
} else {
onUpdateExpandedViewHolder(avh);
}
}
/**
* Returns true if it is running an expanding or collapsing transition, false otherwise.
* @return True if it is running an expanding or collapsing transition, false otherwise.
*/
public boolean isInExpandTransition() {
return mExpandTransition != null;
}
/**
* Returns if expand/collapse animation is supported. When this method returns true,
* {@link #startExpandedTransition(ViewHolder)} will be used. When this method returns false,
* {@link #onUpdateExpandedViewHolder(ViewHolder)} will be called.
* @return True if it is running an expanding or collapsing transition, false otherwise.
*/
public boolean isExpandTransitionSupported() {
return VERSION.SDK_INT >= 21;
}
/**
* Start transition to expand or collapse GuidedActionStylist.
* @param avh When not null, the GuidedActionStylist expands the sub actions of avh. When null
* the GuidedActionStylist will collapse sub actions.
*/
public void startExpandedTransition(ViewHolder avh) {
ViewHolder focusAvh = null; // expand / collapse view holder
final int count = mActionsGridView.getChildCount();
for (int i = 0; i < count; i++) {
ViewHolder vh = (ViewHolder) mActionsGridView
.getChildViewHolder(mActionsGridView.getChildAt(i));
if (avh == null && vh.itemView.getVisibility() == View.VISIBLE) {
// going to collapse this one.
focusAvh = vh;
break;
} else if (avh != null && vh.getAction() == avh.getAction()) {
// going to expand this one.
focusAvh = vh;
break;
}
}
if (focusAvh == null) {
// huh?
onUpdateExpandedViewHolder(avh);
return;
}
boolean isSubActionTransition = focusAvh.getAction().hasSubActions();
Object set = TransitionHelper.createTransitionSet(false);
float slideDistance = isSubActionTransition ? focusAvh.itemView.getHeight() :
focusAvh.itemView.getHeight() * 0.5f;
Object slideAndFade = TransitionHelper.createFadeAndShortSlide(Gravity.TOP | Gravity.BOTTOM,
slideDistance);
Object changeFocusItemTransform = TransitionHelper.createChangeTransform();
Object changeFocusItemBounds = TransitionHelper.createChangeBounds(false);
Object fade = TransitionHelper.createFadeTransition(TransitionHelper.FADE_IN |
TransitionHelper.FADE_OUT);
Object changeGridBounds = TransitionHelper.createChangeBounds(false);
if (avh == null) {
TransitionHelper.setStartDelay(slideAndFade, 150);
TransitionHelper.setStartDelay(changeFocusItemTransform, 100);
TransitionHelper.setStartDelay(changeFocusItemBounds, 100);
} else {
TransitionHelper.setStartDelay(fade, 100);
TransitionHelper.setStartDelay(changeGridBounds, 100);
TransitionHelper.setStartDelay(changeFocusItemTransform, 50);
TransitionHelper.setStartDelay(changeFocusItemBounds, 50);
}
for (int i = 0; i < count; i++) {
ViewHolder vh = (ViewHolder) mActionsGridView
.getChildViewHolder(mActionsGridView.getChildAt(i));
if (vh == focusAvh) {
// going to expand/collapse this one.
if (isSubActionTransition) {
TransitionHelper.include(changeFocusItemTransform, vh.itemView);
TransitionHelper.include(changeFocusItemBounds, vh.itemView);
}
} else {
// going to slide this item to top / bottom.
TransitionHelper.include(slideAndFade, vh.itemView);
TransitionHelper.exclude(fade, vh.itemView, true);
}
}
TransitionHelper.include(changeGridBounds, mSubActionsGridView);
TransitionHelper.include(changeGridBounds, mSubActionsBackground);
TransitionHelper.addTransition(set, slideAndFade);
// note that we don't run ChangeBounds for activating view due to the rounding problem
// of multiple level views ChangeBounds animation causing vertical jittering.
if (isSubActionTransition) {
TransitionHelper.addTransition(set, changeFocusItemTransform);
TransitionHelper.addTransition(set, changeFocusItemBounds);
}
TransitionHelper.addTransition(set, fade);
TransitionHelper.addTransition(set, changeGridBounds);
mExpandTransition = set;
TransitionHelper.addTransitionListener(mExpandTransition, new TransitionListener() {
@Override
public void onTransitionEnd(Object transition) {
mExpandTransition = null;
}
});
if (avh != null && mSubActionsGridView.getTop() != avh.itemView.getTop()) {
// For expanding, set the initial position of subActionsGridView before running
// a ChangeBounds on it.
final ViewHolder toUpdate = avh;
mSubActionsGridView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (mSubActionsGridView == null) {
return;
}
mSubActionsGridView.removeOnLayoutChangeListener(this);
mMainView.post(new Runnable() {
@Override
public void run() {
if (mMainView == null) {
return;
}
TransitionHelper.beginDelayedTransition(mMainView, mExpandTransition);
onUpdateExpandedViewHolder(toUpdate);
}
});
}
});
ViewGroup.MarginLayoutParams lp =
(ViewGroup.MarginLayoutParams) mSubActionsGridView.getLayoutParams();
lp.topMargin = avh.itemView.getTop();
lp.height = 0;
mSubActionsGridView.setLayoutParams(lp);
return;
}
TransitionHelper.beginDelayedTransition(mMainView, mExpandTransition);
onUpdateExpandedViewHolder(avh);
}
/**
* @return True if sub actions list is expanded.
*/
public boolean isSubActionsExpanded() {
return mExpandedAction != null;
}
/**
* @return Current expanded GuidedAction or null if not expanded.
*/
public GuidedAction getExpandedAction() {
return mExpandedAction;
}
/**
* Expand or collapse GuidedActionStylist.
* @param avh When not null, the GuidedActionStylist expands the sub actions of avh. When null
* the GuidedActionStylist will collapse sub actions.
*/
public void onUpdateExpandedViewHolder(ViewHolder avh) {
// Note about setting the prune child flag back & forth here: without this, the actions that
// go off the screen from the top or bottom become invisible forever. This is because once
// an action is expanded, it takes more space which in turn kicks out some other actions
// off of the screen. Once, this action is collapsed (after the second click) and the
// visibility flag is set back to true for all existing actions,
// the off-the-screen actions are pruned from the view, thus
// could not be accessed, had we not disabled pruning prior to this.
if (avh == null) {
mExpandedAction = null;
mActionsGridView.setPruneChild(true);
} else if (avh.getAction() != mExpandedAction) {
mExpandedAction = avh.getAction();
mActionsGridView.setPruneChild(false);
}
// In expanding mode, notifyItemChange on expanded item will reset the translationY by
// the default ItemAnimator. So disable ItemAnimation in expanding mode.
mActionsGridView.setAnimateChildLayout(false);
final int count = mActionsGridView.getChildCount();
for (int i = 0; i < count; i++) {
ViewHolder vh = (ViewHolder) mActionsGridView
.getChildViewHolder(mActionsGridView.getChildAt(i));
updateChevronAndVisibility(vh);
}
if (mSubActionsGridView != null) {
if (avh != null && avh.getAction().hasSubActions()) {
ViewGroup.MarginLayoutParams lp =
(ViewGroup.MarginLayoutParams) mSubActionsGridView.getLayoutParams();
lp.topMargin = avh.itemView.getTop();
lp.height = ViewGroup.MarginLayoutParams.MATCH_PARENT;
mSubActionsGridView.setLayoutParams(lp);
mSubActionsGridView.setVisibility(View.VISIBLE);
mSubActionsBackground.setVisibility(View.VISIBLE);
mSubActionsGridView.requestFocus();
mSubActionsGridView.setSelectedPosition(0);
((GuidedActionAdapter) mSubActionsGridView.getAdapter())
.setActions(avh.getAction().getSubActions());
} else if (mSubActionsGridView.getVisibility() == View.VISIBLE) {
mSubActionsGridView.setVisibility(View.INVISIBLE);
mSubActionsBackground.setVisibility(View.INVISIBLE);
ViewGroup.MarginLayoutParams lp =
(ViewGroup.MarginLayoutParams) mSubActionsGridView.getLayoutParams();
lp.height = 0;
mSubActionsGridView.setLayoutParams(lp);
((GuidedActionAdapter) mSubActionsGridView.getAdapter())
.setActions(Collections.EMPTY_LIST);
mActionsGridView.requestFocus();
}
}
}
private void updateChevronAndVisibility(ViewHolder vh) {
if (!vh.isSubAction()) {
if (mExpandedAction == null) {
vh.itemView.setVisibility(View.VISIBLE);
vh.itemView.setTranslationY(0);
if (vh.mActivatorView != null) {
vh.setActivated(false);
}
} else if (vh.getAction() == mExpandedAction) {
vh.itemView.setVisibility(View.VISIBLE);
if (vh.getAction().hasSubActions()) {
vh.itemView.setTranslationY(- vh.itemView.getHeight());
} else if (vh.mActivatorView != null) {
vh.itemView.setTranslationY(0);
vh.setActivated(true);
}
} else {
vh.itemView.setVisibility(View.INVISIBLE);
vh.itemView.setTranslationY(0);
}
}
if (vh.mChevronView != null) {
onBindChevronView(vh, vh.getAction());
}
}
/*
* ==========================================
* FragmentAnimationProvider overrides
* ==========================================
*/
/**
* {@inheritDoc}
*/
@Override
public void onImeAppearing(@NonNull List