| /* |
| * 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 androidx.leanback.widget; |
| |
| import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; |
| import static androidx.leanback.widget.GuidedAction.EDITING_ACTIVATOR_VIEW; |
| import static androidx.leanback.widget.GuidedAction.EDITING_DESCRIPTION; |
| import static androidx.leanback.widget.GuidedAction.EDITING_NONE; |
| import static androidx.leanback.widget.GuidedAction.EDITING_TITLE; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorInflater; |
| import android.animation.AnimatorListenerAdapter; |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.content.res.TypedArray; |
| import android.graphics.Rect; |
| import android.graphics.drawable.Drawable; |
| import android.os.Build; |
| import android.os.Build.VERSION; |
| import android.text.InputType; |
| import android.text.TextUtils; |
| import android.util.TypedValue; |
| import android.view.Gravity; |
| import android.view.KeyEvent; |
| 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 androidx.annotation.CallSuper; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.RestrictTo; |
| import androidx.core.content.ContextCompat; |
| import androidx.leanback.R; |
| import androidx.leanback.transition.TransitionEpicenterCallback; |
| import androidx.leanback.transition.TransitionHelper; |
| import androidx.leanback.transition.TransitionListener; |
| import androidx.leanback.widget.GuidedActionAdapter.EditListener; |
| import androidx.leanback.widget.picker.DatePicker; |
| import androidx.recyclerview.widget.RecyclerView; |
| |
| import java.util.Calendar; |
| import java.util.Collections; |
| import java.util.List; |
| |
| /** |
| * GuidedActionsStylist is used within a {@link androidx.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". |
| * <p> |
| * 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 |
| * <a href="http://developer.android.com/guide/topics/ui/themes.html">Styles and Themes</a> |
| * for more information. |
| * <p> |
| * 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. |
| * <p> |
| * To support a "click to activate" view similar to DatePicker, app needs: |
| * <li> Override {@link #onProvideItemLayoutId(int)} and {@link #getItemViewType(GuidedAction)}, |
| * provides a layout id for the action. |
| * <li> The layout must include a widget with id "guidedactions_activator_item", the widget is |
| * toggled edit mode by {@link View#setActivated(boolean)}. |
| * <li> Override {@link #onBindActivatorView(ViewHolder, GuidedAction)} to populate values into View. |
| * <li> Override {@link #onUpdateActivatorView(ViewHolder, GuidedAction)} to update action. |
| * <p> |
| * Note: If an alternate list layout is provided, the following view IDs must be supplied: |
| * <ul> |
| * <li>{@link androidx.leanback.R.id#guidedactions_list}</li> |
| * </ul><p> |
| * These view IDs must be present in order for the stylist to function. The list ID must correspond |
| * to a {@link VerticalGridView} or subclass. |
| * <p> |
| * If an alternate item layout is provided, the following view IDs should be used to refer to base |
| * elements: |
| * <ul> |
| * <li>{@link androidx.leanback.R.id#guidedactions_item_content}</li> |
| * <li>{@link androidx.leanback.R.id#guidedactions_item_title}</li> |
| * <li>{@link androidx.leanback.R.id#guidedactions_item_description}</li> |
| * <li>{@link androidx.leanback.R.id#guidedactions_item_icon}</li> |
| * <li>{@link androidx.leanback.R.id#guidedactions_item_checkmark}</li> |
| * <li>{@link androidx.leanback.R.id#guidedactions_item_chevron}</li> |
| * </ul><p> |
| * These view IDs are allowed to be missing, in which case the corresponding views in {@link |
| * GuidedActionsStylist.ViewHolder} will be null. |
| * <p> |
| * 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 and {@link GuidedActionAutofillSupport} interface. |
| * |
| * {@link androidx.leanback.R.attr#guidedStepImeAppearingAnimation} |
| * {@link androidx.leanback.R.attr#guidedStepImeDisappearingAnimation} |
| * {@link androidx.leanback.R.attr#guidedActionsSelectorDrawable} |
| * {@link androidx.leanback.R.attr#guidedActionsListStyle} |
| * {@link androidx.leanback.R.attr#guidedSubActionsListStyle} |
| * {@link androidx.leanback.R.attr#guidedButtonActionsListStyle} |
| * {@link androidx.leanback.R.attr#guidedActionItemContainerStyle} |
| * {@link androidx.leanback.R.attr#guidedActionItemCheckmarkStyle} |
| * {@link androidx.leanback.R.attr#guidedActionItemIconStyle} |
| * {@link androidx.leanback.R.attr#guidedActionItemContentStyle} |
| * {@link androidx.leanback.R.attr#guidedActionItemTitleStyle} |
| * {@link androidx.leanback.R.attr#guidedActionItemDescriptionStyle} |
| * {@link androidx.leanback.R.attr#guidedActionItemChevronStyle} |
| * {@link androidx.leanback.R.attr#guidedActionPressedAnimation} |
| * {@link androidx.leanback.R.attr#guidedActionUnpressedAnimation} |
| * {@link androidx.leanback.R.attr#guidedActionEnabledChevronAlpha} |
| * {@link androidx.leanback.R.attr#guidedActionDisabledChevronAlpha} |
| * {@link androidx.leanback.R.attr#guidedActionTitleMinLines} |
| * {@link androidx.leanback.R.attr#guidedActionTitleMaxLines} |
| * {@link androidx.leanback.R.attr#guidedActionDescriptionMinLines} |
| * {@link androidx.leanback.R.attr#guidedActionVerticalPadding} |
| * @see android.R.attr#listChoiceIndicatorSingle |
| * @see android.R.attr#listChoiceIndicatorMultiple |
| * @see androidx.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; |
| Animator mPressAnimator; |
| |
| 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; |
| } |
| |
| void press(boolean pressed) { |
| if (mPressAnimator != null) { |
| mPressAnimator.cancel(); |
| mPressAnimator = null; |
| } |
| final int themeAttrId = pressed ? R.attr.guidedActionPressedAnimation : |
| R.attr.guidedActionUnpressedAnimation; |
| Context ctx = itemView.getContext(); |
| TypedValue typedValue = new TypedValue(); |
| if (ctx.getTheme().resolveAttribute(themeAttrId, typedValue, true)) { |
| mPressAnimator = AnimatorInflater.loadAnimator(ctx, typedValue.resourceId); |
| mPressAnimator.setTarget(itemView); |
| mPressAnimator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| mPressAnimator = null; |
| } |
| }); |
| mPressAnimator.start(); |
| } |
| } |
| } |
| |
| private static final String TAG = "GuidedActionsStylist"; |
| |
| ViewGroup mMainView; |
| private VerticalGridView mActionsGridView; |
| VerticalGridView mSubActionsGridView; |
| private View mSubActionsBackground; |
| 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; |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| GuidedAction mExpandedAction = null; |
| Object mExpandTransition; |
| private boolean mBackToCollapseSubActions = true; |
| private boolean mBackToCollapseActivatorView = true; |
| |
| private float mKeyLinePercent; |
| |
| /** |
| * Creates a view appropriate for displaying a list of GuidedActions, using the provided |
| * inflater and container. |
| * <p> |
| * <i>Note: Does not actually add the created view to the container; the caller should do |
| * this.</i> |
| * @param inflater The layout inflater to be used when constructing the view. |
| * @param container The view group to be passed in the call to |
| * <code>LayoutInflater.inflate</code>. |
| * @return The view to be added to the caller's view hierarchy. |
| */ |
| @SuppressWarnings("deprecation") /* defaultDisplay */ |
| 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); |
| 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 = getFloatValue(ctx.getResources(), val, R.dimen |
| .lb_guidedactions_item_unselected_text_alpha); |
| mDisabledTextAlpha = getFloatValue(ctx.getResources(), val, R.dimen |
| .lb_guidedactions_item_disabled_text_alpha); |
| mEnabledDescriptionAlpha = getFloatValue(ctx.getResources(), val, R.dimen |
| .lb_guidedactions_item_unselected_description_text_alpha); |
| mDisabledDescriptionAlpha = getFloatValue(ctx.getResources(), val, R.dimen |
| .lb_guidedactions_item_disabled_description_text_alpha); |
| |
| mKeyLinePercent = GuidanceStylingRelativeLayout.getKeyLinePercent(ctx); |
| if (mContentView instanceof GuidedActionsRelativeLayout) { |
| ((GuidedActionsRelativeLayout) mContentView).setInterceptKeyEventListener( |
| new GuidedActionsRelativeLayout.InterceptKeyEventListener() { |
| @Override |
| public boolean onInterceptKeyEvent(KeyEvent event) { |
| if (event.getKeyCode() == KeyEvent.KEYCODE_BACK |
| && event.getAction() == KeyEvent.ACTION_UP |
| && mExpandedAction != null) { |
| if ((mExpandedAction.hasSubActions() |
| && isBackKeyToCollapseSubActions()) |
| || (mExpandedAction.hasEditableActivatorView() |
| && isBackKeyToCollapseActivatorView())) { |
| collapseAction(true); |
| return true; |
| } |
| } |
| return false; |
| } |
| } |
| ); |
| } |
| 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; |
| 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 androidx.leanback.R.layout#lb_guidedactions} or |
| * {@link androidx.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 androidx.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}, |
| * {@link GuidedActionAutofillSupport}; 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: |
| * <li>{@link androidx.leanback.R.layout#lb_guidedactions_item} |
| * <li>{{@link androidx.leanback.R.layout#lb_guidedactions_datepicker_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}. |
| * |
| * @param viewType View type returned by {@link #getItemViewType(GuidedAction)} |
| * @return The resource ID of the layout to be inflated to define the view to display an |
| * individual GuidedAction. |
| */ |
| public int onProvideItemLayoutId(int viewType) { |
| if (viewType == VIEW_TYPE_DEFAULT) { |
| return onProvideItemLayoutId(); |
| } else if (viewType == VIEW_TYPE_DATE_PICKER) { |
| return R.layout.lb_guidedactions_datepicker_item; |
| } else { |
| throw new RuntimeException("ViewType " + viewType |
| + " not supported in GuidedActionsStylist"); |
| } |
| } |
| |
| /** |
| * Constructs a {@link ViewHolder} capable of representing {@link GuidedAction}s. Subclasses |
| * may choose to return a subclass of ViewHolder. To support different view types, override |
| * {@link #onCreateViewHolder(ViewGroup, int)} |
| * <p> |
| * <i>Note: Should not actually add the created view to the parent; the caller will do |
| * this.</i> |
| * @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. |
| * <p> |
| * <i>Note: Should not actually add the created view to the parent; the caller will do |
| * this.</i> |
| * @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.setInputType(action.getInputType()); |
| 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 (Build.VERSION.SDK_INT >= 28) { |
| if (action.isEditable()) { |
| vh.mTitleView.setAutofillHints(action.getAutofillHints()); |
| } else { |
| vh.mTitleView.setAutofillHints((String[]) null); |
| } |
| } else if (VERSION.SDK_INT >= 26) { |
| // disable autofill below P as dpad/keyboard is not supported |
| vh.mTitleView.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO); |
| } |
| } |
| if (vh.mDescriptionView != null) { |
| vh.mDescriptionView.setInputType(action.getDescriptionInputType()); |
| 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); |
| if (Build.VERSION.SDK_INT >= 28) { |
| if (action.isDescriptionEditable()) { |
| vh.mDescriptionView.setAutofillHints(action.getAutofillHints()); |
| } else { |
| vh.mDescriptionView.setAutofillHints((String[]) null); |
| } |
| } else if (VERSION.SDK_INT >= 26) { |
| // disable autofill below P as dpad/keyboard is not supported |
| vh.mTitleView.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO); |
| } |
| } |
| // 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); |
| vh.mTitleView.setInputType( |
| vh.mTitleView.getInputType() | InputType.TYPE_TEXT_FLAG_MULTI_LINE); |
| if (vh.mDescriptionView != null) { |
| vh.mDescriptionView.setInputType(vh.mDescriptionView.getInputType() |
| | InputType.TYPE_TEXT_FLAG_MULTI_LINE); |
| vh.mDescriptionView.setMaxHeight(getDescriptionMaxHeight(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, false /*editing*/, false /*withTransition*/); |
| 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); |
| } |
| |
| /** |
| * Switches action to edit mode and pops up the keyboard. |
| */ |
| public void openInEditMode(GuidedAction action) { |
| final GuidedActionAdapter guidedActionAdapter = |
| (GuidedActionAdapter) getActionsGridView().getAdapter(); |
| int actionIndex = guidedActionAdapter.getActions().indexOf(action); |
| if (actionIndex < 0 || !action.isEditable()) { |
| return; |
| } |
| |
| getActionsGridView().setSelectedPosition(actionIndex, new ViewHolderTask() { |
| @Override |
| public void run(RecyclerView.ViewHolder viewHolder) { |
| ViewHolder vh = (ViewHolder) viewHolder; |
| guidedActionAdapter.mGroup.openIme(guidedActionAdapter, 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); |
| } |
| } |
| |
| /** |
| * @deprecated This method is for internal library use only and should not |
| * be called directly. |
| */ |
| @Deprecated |
| public void setEditingMode(ViewHolder vh, GuidedAction action, boolean editing) { |
| if (editing != vh.isInEditing() && isInExpandTransition()) { |
| onEditingModeChange(vh, action, editing); |
| } |
| } |
| |
| void setEditingMode(ViewHolder vh, boolean editing) { |
| setEditingMode(vh, editing, true /*withTransition*/); |
| } |
| |
| void setEditingMode(ViewHolder vh, boolean editing, boolean withTransition) { |
| if (editing != vh.isInEditing() && !isInExpandTransition()) { |
| onEditingModeChange(vh, editing, withTransition); |
| } |
| } |
| |
| /** |
| * @deprecated Use {@link #onEditingModeChange(ViewHolder, boolean, boolean)}. |
| */ |
| @Deprecated |
| protected void onEditingModeChange(ViewHolder vh, GuidedAction action, boolean editing) { |
| } |
| |
| /** |
| * Called when editing mode of an ViewHolder is changed. Subclass must call |
| * <code>super.onEditingModeChange(vh,editing,withTransition)</code>. |
| * |
| * @param vh ViewHolder to change editing mode. |
| * @param editing True to enable editing, false to stop editing |
| * @param withTransition True to run expand transiiton, false otherwise. |
| */ |
| @CallSuper |
| protected void onEditingModeChange(ViewHolder vh, boolean editing, boolean withTransition) { |
| GuidedAction 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()); |
| descriptionView.requestFocusFromTouch(); |
| } |
| vh.mEditingMode = EDITING_DESCRIPTION; |
| } else if (action.isEditable()){ |
| if (titleView != null) { |
| titleView.setInputType(action.getEditInputType()); |
| titleView.requestFocusFromTouch(); |
| } |
| vh.mEditingMode = EDITING_TITLE; |
| } else if (vh.mActivatorView != null) { |
| onEditActivatorView(vh, editing, withTransition); |
| 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, editing, withTransition); |
| } |
| } |
| vh.mEditingMode = EDITING_NONE; |
| } |
| // call deprecated method for backward compatible |
| onEditingModeChange(vh, action, editing); |
| } |
| |
| /** |
| * 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) { |
| vh.press(pressed); |
| } |
| |
| /** |
| * Resets the view holder's view to unpressed state. |
| * @param vh The view holder associated with the relevant action. |
| */ |
| public void onAnimateItemPressedCancelled(ViewHolder vh) { |
| vh.press(false); |
| } |
| |
| /** |
| * 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 {androidx.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.setDate(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(LIBRARY_GROUP_PREFIX) |
| public void setEditListener(EditListener listener) { |
| mEditListener = listener; |
| } |
| |
| void onEditActivatorView(final ViewHolder vh, boolean editing, final boolean withTransition) { |
| if (editing) { |
| startExpanded(vh, withTransition); |
| vh.itemView.setFocusable(false); |
| vh.mActivatorView.requestFocus(); |
| vh.mActivatorView.setOnClickListener(new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| if (!isInExpandTransition()) { |
| ((GuidedActionAdapter) getActionsGridView().getAdapter()) |
| .performOnActionClick(vh); |
| } |
| } |
| }); |
| } else { |
| if (onUpdateActivatorView(vh, vh.getAction())) { |
| if (mEditListener != null) { |
| mEditListener.onGuidedActionEditedAndProceed(vh.getAction()); |
| } |
| } |
| vh.itemView.setFocusable(true); |
| vh.itemView.requestFocus(); |
| startExpanded(null, withTransition); |
| 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 with transition animation |
| * @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. |
| * @deprecated use {@link #expandAction(GuidedAction, boolean)} and |
| * {@link #collapseAction(boolean)} |
| */ |
| @Deprecated |
| public void setExpandedViewHolder(ViewHolder avh) { |
| expandAction(avh == null ? null : avh.getAction(), isExpandTransitionSupported()); |
| } |
| |
| /** |
| * 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. |
| * @deprecated use {@link #expandAction(GuidedAction, boolean)} and |
| * {@link #collapseAction(boolean)} |
| */ |
| @Deprecated |
| public void startExpandedTransition(ViewHolder avh) { |
| expandAction(avh == null ? null : avh.getAction(), isExpandTransitionSupported()); |
| } |
| |
| /** |
| * Enable or disable using BACK key to collapse sub actions list. Default is enabled. |
| * |
| * @param backToCollapse True to enable using BACK key to collapse sub actions list, false |
| * to disable. |
| * @see GuidedAction#hasSubActions |
| * @see GuidedAction#getSubActions |
| */ |
| public final void setBackKeyToCollapseSubActions(boolean backToCollapse) { |
| mBackToCollapseSubActions = backToCollapse; |
| } |
| |
| /** |
| * @return True if using BACK key to collapse sub actions list, false otherwise. Default value |
| * is true. |
| * |
| * @see GuidedAction#hasSubActions |
| * @see GuidedAction#getSubActions |
| */ |
| public final boolean isBackKeyToCollapseSubActions() { |
| return mBackToCollapseSubActions; |
| } |
| |
| /** |
| * Enable or disable using BACK key to collapse {@link GuidedAction} with editable activator |
| * view. Default is enabled. |
| * |
| * @param backToCollapse True to enable using BACK key to collapse {@link GuidedAction} with |
| * editable activator view. |
| * @see GuidedAction#hasEditableActivatorView |
| */ |
| public final void setBackKeyToCollapseActivatorView(boolean backToCollapse) { |
| mBackToCollapseActivatorView = backToCollapse; |
| } |
| |
| /** |
| * @return True if using BACK key to collapse {@link GuidedAction} with editable activator |
| * view, false otherwise. Default value is true. |
| * |
| * @see GuidedAction#hasEditableActivatorView |
| */ |
| public final boolean isBackKeyToCollapseActivatorView() { |
| return mBackToCollapseActivatorView; |
| } |
| |
| /** |
| * Expand an action. Do nothing if it is in animation or there is action expanded. |
| * |
| * @param action Action to expand. |
| * @param withTransition True to run transition animation, false otherwsie. |
| */ |
| public void expandAction(GuidedAction action, final boolean withTransition) { |
| if (isInExpandTransition() || mExpandedAction != null) { |
| return; |
| } |
| int actionPosition = |
| ((GuidedActionAdapter) getActionsGridView().getAdapter()).indexOf(action); |
| if (actionPosition < 0) { |
| return; |
| } |
| boolean runTransition = isExpandTransitionSupported() && withTransition; |
| if (!runTransition) { |
| getActionsGridView().setSelectedPosition(actionPosition, |
| new ViewHolderTask() { |
| @Override |
| public void run(RecyclerView.ViewHolder vh) { |
| GuidedActionsStylist.ViewHolder avh = |
| (GuidedActionsStylist.ViewHolder)vh; |
| if (avh.getAction().hasEditableActivatorView()) { |
| setEditingMode(avh, true /*editing*/, false /*withTransition*/); |
| } else { |
| onUpdateExpandedViewHolder(avh); |
| } |
| } |
| }); |
| if (action.hasSubActions()) { |
| onUpdateSubActionsGridView(action, true); |
| } |
| } else { |
| getActionsGridView().setSelectedPosition(actionPosition, |
| new ViewHolderTask() { |
| @Override |
| public void run(RecyclerView.ViewHolder vh) { |
| GuidedActionsStylist.ViewHolder avh = |
| (GuidedActionsStylist.ViewHolder)vh; |
| if (avh.getAction().hasEditableActivatorView()) { |
| setEditingMode(avh, true /*editing*/, true /*withTransition*/); |
| } else { |
| startExpanded(avh, true); |
| } |
| } |
| }); |
| } |
| |
| } |
| |
| /** |
| * Collapse expanded action. Do nothing if it is in animation or there is no action expanded. |
| * |
| * @param withTransition True to run transition animation, false otherwsie. |
| */ |
| public void collapseAction(boolean withTransition) { |
| if (isInExpandTransition() || mExpandedAction == null) { |
| return; |
| } |
| boolean runTransition = isExpandTransitionSupported() && withTransition; |
| int actionPosition = |
| ((GuidedActionAdapter) getActionsGridView().getAdapter()).indexOf(mExpandedAction); |
| if (actionPosition < 0) { |
| return; |
| } |
| if (mExpandedAction.hasEditableActivatorView()) { |
| setEditingMode( |
| ((ViewHolder) getActionsGridView().findViewHolderForPosition(actionPosition)), |
| false /*editing*/, |
| runTransition); |
| } else { |
| startExpanded(null, runTransition); |
| } |
| } |
| |
| int getKeyLine() { |
| return (int) (mKeyLinePercent * mActionsGridView.getHeight() / 100); |
| } |
| |
| /** |
| * Internal method with assumption we already scroll to the new ViewHolder or is currently |
| * expanded. |
| */ |
| void startExpanded(ViewHolder avh, final boolean withTransition) { |
| 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? |
| return; |
| } |
| boolean isExpand = avh != null; |
| boolean isSubActionTransition = focusAvh.getAction().hasSubActions(); |
| if (withTransition) { |
| 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); |
| TransitionHelper.setEpicenterCallback(slideAndFade, new TransitionEpicenterCallback() { |
| Rect mRect = new Rect(); |
| @Override |
| public Rect onGetEpicenter(Object transition) { |
| int centerY = getKeyLine(); |
| int centerX = 0; |
| mRect.set(centerX, centerY, centerX, centerY); |
| return mRect; |
| } |
| }); |
| 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); |
| TransitionHelper.setStartDelay(changeGridBounds, 100); |
| } else { |
| TransitionHelper.setStartDelay(fade, 100); |
| TransitionHelper.setStartDelay(changeGridBounds, 50); |
| 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 (isExpand && isSubActionTransition) { |
| // To expand sub actions, move original position of sub actions to bottom of item |
| int startY = avh.itemView.getBottom(); |
| mSubActionsGridView.offsetTopAndBottom(startY - mSubActionsGridView.getTop()); |
| mSubActionsBackground.offsetTopAndBottom(startY - mSubActionsBackground.getTop()); |
| } |
| TransitionHelper.beginDelayedTransition(mMainView, mExpandTransition); |
| } |
| onUpdateExpandedViewHolder(avh); |
| if (isSubActionTransition) { |
| onUpdateSubActionsGridView(focusAvh.getAction(), isExpand); |
| } |
| } |
| |
| /** |
| * @return True if sub actions list is expanded. |
| */ |
| public boolean isSubActionsExpanded() { |
| return mExpandedAction != null && mExpandedAction.hasSubActions(); |
| } |
| |
| /** |
| * @return True if there is {@link #getExpandedAction()} is not null, false otherwise. |
| */ |
| public boolean isExpanded() { |
| 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); |
| } |
| } |
| |
| void onUpdateSubActionsGridView(GuidedAction action, boolean expand) { |
| if (mSubActionsGridView != null) { |
| ViewGroup.MarginLayoutParams lp = |
| (ViewGroup.MarginLayoutParams) mSubActionsGridView.getLayoutParams(); |
| GuidedActionAdapter adapter = (GuidedActionAdapter) mSubActionsGridView.getAdapter(); |
| if (expand) { |
| // set to negative value so GuidedActionRelativeLayout will override with |
| // keyLine percentage. |
| lp.topMargin = -2; |
| lp.height = ViewGroup.MarginLayoutParams.MATCH_PARENT; |
| mSubActionsGridView.setLayoutParams(lp); |
| mSubActionsGridView.setVisibility(View.VISIBLE); |
| mSubActionsBackground.setVisibility(View.VISIBLE); |
| mSubActionsGridView.requestFocus(); |
| adapter.setActions(action.getSubActions()); |
| } else { |
| // set to explicit value, which will disable the keyLine percentage calculation |
| // in GuidedRelativeLayout. |
| int actionPosition = ((GuidedActionAdapter) mActionsGridView.getAdapter()) |
| .indexOf(action); |
| lp.topMargin = mActionsGridView.getLayoutManager() |
| .findViewByPosition(actionPosition).getBottom(); |
| lp.height = 0; |
| mSubActionsGridView.setVisibility(View.INVISIBLE); |
| mSubActionsBackground.setVisibility(View.INVISIBLE); |
| mSubActionsGridView.setLayoutParams(lp); |
| adapter.setActions(Collections.<GuidedAction>emptyList()); |
| 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(getKeyLine() - vh.itemView.getBottom()); |
| } 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<Animator> animators) { |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void onImeDisappearing(@NonNull List<Animator> animators) { |
| } |
| |
| /* |
| * ========================================== |
| * Private methods |
| * ========================================== |
| */ |
| |
| private static float getFloat(Context ctx, TypedValue typedValue, int attrId) { |
| ctx.getTheme().resolveAttribute(attrId, typedValue, true); |
| return typedValue.getFloat(); |
| } |
| |
| private static float getFloatValue(Resources resources, TypedValue typedValue, int resId) { |
| resources.getValue(resId, typedValue, true); |
| return typedValue.getFloat(); |
| } |
| |
| private static int getInteger(Context ctx, TypedValue typedValue, int attrId) { |
| ctx.getTheme().resolveAttribute(attrId, typedValue, true); |
| return ctx.getResources().getInteger(typedValue.resourceId); |
| } |
| |
| private static int getDimension(Context ctx, TypedValue typedValue, int attrId) { |
| ctx.getTheme().resolveAttribute(attrId, typedValue, true); |
| return ctx.getResources().getDimensionPixelSize(typedValue.resourceId); |
| } |
| |
| private boolean setIcon(final ImageView iconView, GuidedAction action) { |
| Drawable icon = null; |
| if (iconView != null) { |
| icon = action.getIcon(); |
| if (icon != null) { |
| // setImageDrawable resets the drawable's level unless we set the view level first. |
| iconView.setImageLevel(icon.getLevel()); |
| iconView.setImageDrawable(icon); |
| iconView.setVisibility(View.VISIBLE); |
| } else { |
| iconView.setVisibility(View.GONE); |
| } |
| } |
| return icon != null; |
| } |
| |
| /** |
| * @return the max height in pixels the description can be such that the |
| * action nicely takes up the entire screen. |
| */ |
| private int getDescriptionMaxHeight(TextView title) { |
| // The 2 multiplier on the title height calculation is a |
| // conservative estimate for font padding which can not be |
| // calculated at this stage since the view hasn't been rendered yet. |
| return (int)(mDisplayHeight - 2*mVerticalPadding - 2*mTitleMaxLines*title.getLineHeight()); |
| } |
| |
| } |