1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14 package androidx.leanback.widget; 15 16 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; 17 import static androidx.leanback.widget.GuidedAction.EDITING_ACTIVATOR_VIEW; 18 import static androidx.leanback.widget.GuidedAction.EDITING_DESCRIPTION; 19 import static androidx.leanback.widget.GuidedAction.EDITING_NONE; 20 import static androidx.leanback.widget.GuidedAction.EDITING_TITLE; 21 22 import android.animation.Animator; 23 import android.animation.AnimatorInflater; 24 import android.animation.AnimatorListenerAdapter; 25 import android.content.Context; 26 import android.content.res.Resources; 27 import android.content.res.TypedArray; 28 import android.graphics.Rect; 29 import android.graphics.drawable.Drawable; 30 import android.os.Build; 31 import android.os.Build.VERSION; 32 import android.text.InputType; 33 import android.text.TextUtils; 34 import android.util.TypedValue; 35 import android.view.Gravity; 36 import android.view.KeyEvent; 37 import android.view.LayoutInflater; 38 import android.view.View; 39 import android.view.View.AccessibilityDelegate; 40 import android.view.ViewGroup; 41 import android.view.WindowManager; 42 import android.view.accessibility.AccessibilityEvent; 43 import android.view.accessibility.AccessibilityNodeInfo; 44 import android.view.inputmethod.EditorInfo; 45 import android.widget.Checkable; 46 import android.widget.EditText; 47 import android.widget.ImageView; 48 import android.widget.TextView; 49 50 import androidx.annotation.CallSuper; 51 import androidx.annotation.RequiresApi; 52 import androidx.annotation.RestrictTo; 53 import androidx.core.content.ContextCompat; 54 import androidx.leanback.R; 55 import androidx.leanback.transition.TransitionEpicenterCallback; 56 import androidx.leanback.transition.TransitionHelper; 57 import androidx.leanback.transition.TransitionListener; 58 import androidx.leanback.widget.GuidedActionAdapter.EditListener; 59 import androidx.leanback.widget.picker.DatePicker; 60 import androidx.recyclerview.widget.RecyclerView; 61 62 import org.jspecify.annotations.NonNull; 63 import org.jspecify.annotations.Nullable; 64 65 import java.util.Calendar; 66 import java.util.Collections; 67 import java.util.List; 68 69 /** 70 * GuidedActionsStylist is used within a {@link androidx.leanback.app.GuidedStepFragment} 71 * to supply the right-side panel where users can take actions. It consists of a container for the 72 * list of actions, and a stationary selector view that indicates visually the location of focus. 73 * GuidedActionsStylist has two different layouts: default is for normal actions including text, 74 * radio, checkbox, DatePicker, etc, the other when {@link #setAsButtonActions()} is called is 75 * recommended for button actions such as "yes", "no". 76 * <p> 77 * Many aspects of the base GuidedActionsStylist can be customized through theming; see the 78 * theme attributes below. Note that these attributes are not set on individual elements in layout 79 * XML, but instead would be set in a custom theme. See 80 * <a href="http://developer.android.com/guide/topics/ui/themes.html">Styles and Themes</a> 81 * for more information. 82 * <p> 83 * If these hooks are insufficient, this class may also be subclassed. Subclasses may wish to 84 * override the {@link #onProvideLayoutId} method to change the layout used to display the 85 * list container and selector; override {@link #onProvideItemLayoutId(int)} and 86 * {@link #getItemViewType(GuidedAction)} method to change the layout used to display each action. 87 * <p> 88 * To support a "click to activate" view similar to DatePicker, app needs: 89 * <ul> 90 * <li> Override {@link #onProvideItemLayoutId(int)} and {@link #getItemViewType(GuidedAction)}, 91 * provides a layout id for the action.</li> 92 * <li> The layout must include a widget with id "guidedactions_activator_item", the widget is 93 * toggled edit mode by {@link View#setActivated(boolean)}.</li> 94 * <li> 95 * Override {@link #onBindActivatorView(ViewHolder, GuidedAction)} to populate values into View. 96 * </li> 97 * <li> 98 * Override {@link #onUpdateActivatorView(ViewHolder, GuidedAction)} to update action. 99 * </li> 100 * </ul> 101 * <p> 102 * Note: If an alternate list layout is provided, the following view IDs must be supplied: 103 * <ul> 104 * <li>{@link androidx.leanback.R.id#guidedactions_list}</li> 105 * </ul><p> 106 * These view IDs must be present in order for the stylist to function. The list ID must correspond 107 * to a {@link VerticalGridView} or subclass. 108 * <p> 109 * If an alternate item layout is provided, the following view IDs should be used to refer to base 110 * elements: 111 * <ul> 112 * <li>{@link androidx.leanback.R.id#guidedactions_item_content}</li> 113 * <li>{@link androidx.leanback.R.id#guidedactions_item_title}</li> 114 * <li>{@link androidx.leanback.R.id#guidedactions_item_description}</li> 115 * <li>{@link androidx.leanback.R.id#guidedactions_item_icon}</li> 116 * <li>{@link androidx.leanback.R.id#guidedactions_item_checkmark}</li> 117 * <li>{@link androidx.leanback.R.id#guidedactions_item_chevron}</li> 118 * </ul><p> 119 * These view IDs are allowed to be missing, in which case the corresponding views in {@link 120 * GuidedActionsStylist.ViewHolder} will be null. 121 * <p> 122 * In order to support editable actions, the view associated with guidedactions_item_title should 123 * be a subclass of {@link android.widget.EditText}, and should satisfy the {@link 124 * ImeKeyMonitor} interface and {@link GuidedActionAutofillSupport} interface. 125 * 126 * {@link androidx.leanback.R.attr#guidedStepImeAppearingAnimation} 127 * {@link androidx.leanback.R.attr#guidedStepImeDisappearingAnimation} 128 * {@link androidx.leanback.R.attr#guidedActionsSelectorDrawable} 129 * {@link androidx.leanback.R.attr#guidedActionsListStyle} 130 * {@link androidx.leanback.R.attr#guidedSubActionsListStyle} 131 * {@link androidx.leanback.R.attr#guidedButtonActionsListStyle} 132 * {@link androidx.leanback.R.attr#guidedActionItemContainerStyle} 133 * {@link androidx.leanback.R.attr#guidedActionItemCheckmarkStyle} 134 * {@link androidx.leanback.R.attr#guidedActionItemIconStyle} 135 * {@link androidx.leanback.R.attr#guidedActionItemContentStyle} 136 * {@link androidx.leanback.R.attr#guidedActionItemTitleStyle} 137 * {@link androidx.leanback.R.attr#guidedActionItemDescriptionStyle} 138 * {@link androidx.leanback.R.attr#guidedActionItemChevronStyle} 139 * {@link androidx.leanback.R.attr#guidedActionPressedAnimation} 140 * {@link androidx.leanback.R.attr#guidedActionUnpressedAnimation} 141 * {@link androidx.leanback.R.attr#guidedActionEnabledChevronAlpha} 142 * {@link androidx.leanback.R.attr#guidedActionDisabledChevronAlpha} 143 * {@link androidx.leanback.R.attr#guidedActionTitleMinLines} 144 * {@link androidx.leanback.R.attr#guidedActionTitleMaxLines} 145 * {@link androidx.leanback.R.attr#guidedActionDescriptionMinLines} 146 * {@link androidx.leanback.R.attr#guidedActionVerticalPadding} 147 * @see android.R.attr#listChoiceIndicatorSingle 148 * @see android.R.attr#listChoiceIndicatorMultiple 149 * @see androidx.leanback.app.GuidedStepFragment 150 * @see GuidedAction 151 */ 152 public class GuidedActionsStylist implements FragmentAnimationProvider { 153 154 /** 155 * Default viewType that associated with default layout Id for the action item. 156 * @see #getItemViewType(GuidedAction) 157 * @see #onProvideItemLayoutId(int) 158 * @see #onCreateViewHolder(ViewGroup, int) 159 */ 160 public static final int VIEW_TYPE_DEFAULT = 0; 161 162 /** 163 * ViewType for DatePicker. 164 */ 165 public static final int VIEW_TYPE_DATE_PICKER = 1; 166 167 final static ItemAlignmentFacet sGuidedActionItemAlignFacet; 168 169 static { 170 sGuidedActionItemAlignFacet = new ItemAlignmentFacet(); 171 ItemAlignmentFacet.ItemAlignmentDef alignedDef = new ItemAlignmentFacet.ItemAlignmentDef(); 172 alignedDef.setItemAlignmentViewId(R.id.guidedactions_item_title); 173 alignedDef.setAlignedToTextViewBaseline(true); 174 alignedDef.setItemAlignmentOffset(0); 175 alignedDef.setItemAlignmentOffsetWithPadding(true); 176 alignedDef.setItemAlignmentOffsetPercent(0); sGuidedActionItemAlignFacet.setAlignmentDefs(new ItemAlignmentFacet.ItemAlignmentDef[]{alignedDef})177 sGuidedActionItemAlignFacet.setAlignmentDefs(new ItemAlignmentFacet.ItemAlignmentDef[]{alignedDef}); 178 } 179 180 /** 181 * ViewHolder caches information about the action item layouts' subviews. Subclasses of {@link 182 * GuidedActionsStylist} may also wish to subclass this in order to add fields. 183 * @see GuidedAction 184 */ 185 public static class ViewHolder extends RecyclerView.ViewHolder implements FacetProvider { 186 187 GuidedAction mAction; 188 private View mContentView; 189 TextView mTitleView; 190 TextView mDescriptionView; 191 View mActivatorView; 192 ImageView mIconView; 193 ImageView mCheckmarkView; 194 ImageView mChevronView; 195 int mEditingMode = EDITING_NONE; 196 private final boolean mIsSubAction; 197 Animator mPressAnimator; 198 199 final AccessibilityDelegate mDelegate = new AccessibilityDelegate() { 200 @Override 201 public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { 202 super.onInitializeAccessibilityEvent(host, event); 203 event.setChecked(mAction != null && mAction.isChecked()); 204 } 205 206 @Override 207 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { 208 super.onInitializeAccessibilityNodeInfo(host, info); 209 info.setCheckable( 210 mAction != null && mAction.getCheckSetId() != GuidedAction.NO_CHECK_SET); 211 info.setChecked(mAction != null && mAction.isChecked()); 212 } 213 }; 214 215 /** 216 * Constructs an ViewHolder and caches the relevant subviews. 217 */ ViewHolder(@onNull View v)218 public ViewHolder(@NonNull View v) { 219 this(v, false); 220 } 221 222 /** 223 * Constructs an ViewHolder for sub action and caches the relevant subviews. 224 */ ViewHolder(@onNull View v, boolean isSubAction)225 public ViewHolder(@NonNull View v, boolean isSubAction) { 226 super(v); 227 228 mContentView = v.findViewById(R.id.guidedactions_item_content); 229 mTitleView = (TextView) v.findViewById(R.id.guidedactions_item_title); 230 mActivatorView = v.findViewById(R.id.guidedactions_activator_item); 231 mDescriptionView = (TextView) v.findViewById(R.id.guidedactions_item_description); 232 mIconView = (ImageView) v.findViewById(R.id.guidedactions_item_icon); 233 mCheckmarkView = (ImageView) v.findViewById(R.id.guidedactions_item_checkmark); 234 mChevronView = (ImageView) v.findViewById(R.id.guidedactions_item_chevron); 235 mIsSubAction = isSubAction; 236 237 v.setAccessibilityDelegate(mDelegate); 238 } 239 240 /** 241 * Returns the content view within this view holder's view, where title and description are 242 * shown. 243 */ getContentView()244 public @Nullable View getContentView() { 245 return mContentView; 246 } 247 248 /** 249 * Returns the title view within this view holder's view. 250 */ getTitleView()251 public @Nullable TextView getTitleView() { 252 return mTitleView; 253 } 254 255 /** 256 * Convenience method to return an editable version of the title, if possible, 257 * or null if the title view isn't an EditText. 258 */ getEditableTitleView()259 public @Nullable EditText getEditableTitleView() { 260 return (mTitleView instanceof EditText) ? (EditText)mTitleView : null; 261 } 262 263 /** 264 * Returns the description view within this view holder's view. 265 */ getDescriptionView()266 public @Nullable TextView getDescriptionView() { 267 return mDescriptionView; 268 } 269 270 /** 271 * Convenience method to return an editable version of the description, if possible, 272 * or null if the description view isn't an EditText. 273 */ getEditableDescriptionView()274 public @Nullable EditText getEditableDescriptionView() { 275 return (mDescriptionView instanceof EditText) ? (EditText)mDescriptionView : null; 276 } 277 278 /** 279 * Returns the icon view within this view holder's view. 280 */ getIconView()281 public @Nullable ImageView getIconView() { 282 return mIconView; 283 } 284 285 /** 286 * Returns the checkmark view within this view holder's view. 287 */ getCheckmarkView()288 public @Nullable ImageView getCheckmarkView() { 289 return mCheckmarkView; 290 } 291 292 /** 293 * Returns the chevron view within this view holder's view. 294 */ getChevronView()295 public @Nullable ImageView getChevronView() { 296 return mChevronView; 297 } 298 299 /** 300 * Returns true if in editing title, description, or activator View, false otherwise. 301 */ isInEditing()302 public boolean isInEditing() { 303 return mEditingMode != EDITING_NONE; 304 } 305 306 /** 307 * Returns true if in editing title, description, so IME would be open. 308 * @return True if in editing title, description, so IME would be open, false otherwise. 309 */ isInEditingText()310 public boolean isInEditingText() { 311 return mEditingMode == EDITING_TITLE || mEditingMode == EDITING_DESCRIPTION; 312 } 313 314 /** 315 * Returns true if the TextView is in editing title, false otherwise. 316 */ isInEditingTitle()317 public boolean isInEditingTitle() { 318 return mEditingMode == EDITING_TITLE; 319 } 320 321 /** 322 * Returns true if the TextView is in editing description, false otherwise. 323 */ isInEditingDescription()324 public boolean isInEditingDescription() { 325 return mEditingMode == EDITING_DESCRIPTION; 326 } 327 328 /** 329 * Returns true if is in editing activator view with id guidedactions_activator_item, false 330 * otherwise. 331 */ isInEditingActivatorView()332 public boolean isInEditingActivatorView() { 333 return mEditingMode == EDITING_ACTIVATOR_VIEW; 334 } 335 336 /** 337 * @return Current editing title view or description view or activator view or null if not 338 * in editing. 339 */ getEditingView()340 public @Nullable View getEditingView() { 341 switch(mEditingMode) { 342 case EDITING_TITLE: 343 return mTitleView; 344 case EDITING_DESCRIPTION: 345 return mDescriptionView; 346 case EDITING_ACTIVATOR_VIEW: 347 return mActivatorView; 348 case EDITING_NONE: 349 default: 350 return null; 351 } 352 } 353 354 /** 355 * @return True if bound action is inside {@link GuidedAction#getSubActions()}, false 356 * otherwise. 357 */ isSubAction()358 public boolean isSubAction() { 359 return mIsSubAction; 360 } 361 362 /** 363 * @return Currently bound action. 364 */ getAction()365 public @Nullable GuidedAction getAction() { 366 return mAction; 367 } 368 setActivated(boolean activated)369 void setActivated(boolean activated) { 370 mActivatorView.setActivated(activated); 371 if (itemView instanceof GuidedActionItemContainer) { 372 ((GuidedActionItemContainer) itemView).setFocusOutAllowed(!activated); 373 } 374 } 375 376 @Override getFacet(@onNull Class<?> facetClass)377 public @Nullable Object getFacet(@NonNull Class<?> facetClass) { 378 if (facetClass == ItemAlignmentFacet.class) { 379 return sGuidedActionItemAlignFacet; 380 } 381 return null; 382 } 383 press(boolean pressed)384 void press(boolean pressed) { 385 if (mPressAnimator != null) { 386 mPressAnimator.cancel(); 387 mPressAnimator = null; 388 } 389 final int themeAttrId = pressed ? R.attr.guidedActionPressedAnimation : 390 R.attr.guidedActionUnpressedAnimation; 391 Context ctx = itemView.getContext(); 392 TypedValue typedValue = new TypedValue(); 393 if (ctx.getTheme().resolveAttribute(themeAttrId, typedValue, true)) { 394 mPressAnimator = AnimatorInflater.loadAnimator(ctx, typedValue.resourceId); 395 mPressAnimator.setTarget(itemView); 396 mPressAnimator.addListener(new AnimatorListenerAdapter() { 397 @Override 398 public void onAnimationEnd(Animator animation) { 399 mPressAnimator = null; 400 } 401 }); 402 mPressAnimator.start(); 403 } 404 } 405 } 406 407 private static final String TAG = "GuidedActionsStylist"; 408 409 ViewGroup mMainView; 410 private VerticalGridView mActionsGridView; 411 VerticalGridView mSubActionsGridView; 412 private View mSubActionsBackground; 413 private View mContentView; 414 private boolean mButtonActions; 415 416 // Cached values from resources 417 private float mEnabledTextAlpha; 418 private float mDisabledTextAlpha; 419 private float mEnabledDescriptionAlpha; 420 private float mDisabledDescriptionAlpha; 421 private float mEnabledChevronAlpha; 422 private float mDisabledChevronAlpha; 423 private int mTitleMinLines; 424 private int mTitleMaxLines; 425 private int mDescriptionMinLines; 426 private int mVerticalPadding; 427 private int mDisplayHeight; 428 429 private EditListener mEditListener; 430 431 @SuppressWarnings("WeakerAccess") /* synthetic access */ 432 GuidedAction mExpandedAction = null; 433 Object mExpandTransition; 434 private boolean mBackToCollapseSubActions = true; 435 private boolean mBackToCollapseActivatorView = true; 436 437 private float mKeyLinePercent; 438 439 /** 440 * Creates a view appropriate for displaying a list of GuidedActions, using the provided 441 * inflater and container. 442 * <p> 443 * <i>Note: Does not actually add the created view to the container; the caller should do 444 * this.</i> 445 * @param inflater The layout inflater to be used when constructing the view. 446 * @param container The view group to be passed in the call to 447 * <code>LayoutInflater.inflate</code>. 448 * @return The view to be added to the caller's view hierarchy. 449 */ 450 @SuppressWarnings("deprecation") /* defaultDisplay */ onCreateView(@onNull LayoutInflater inflater, final @NonNull ViewGroup container)451 public @NonNull View onCreateView(@NonNull LayoutInflater inflater, 452 final @NonNull ViewGroup container) { 453 TypedArray ta = inflater.getContext().getTheme().obtainStyledAttributes( 454 R.styleable.LeanbackGuidedStepTheme); 455 float keylinePercent = ta.getFloat(R.styleable.LeanbackGuidedStepTheme_guidedStepKeyline, 456 40); 457 mMainView = (ViewGroup) inflater.inflate(onProvideLayoutId(), container, false); 458 mContentView = mMainView.findViewById(mButtonActions ? R.id.guidedactions_content2 : 459 R.id.guidedactions_content); 460 if (mMainView instanceof VerticalGridView) { 461 mActionsGridView = (VerticalGridView) mMainView; 462 } else { 463 mActionsGridView = (VerticalGridView) mMainView.findViewById(mButtonActions 464 ? R.id.guidedactions_list2 : R.id.guidedactions_list); 465 if (mActionsGridView == null) { 466 throw new IllegalStateException("No ListView exists."); 467 } 468 mActionsGridView.setWindowAlignmentOffsetPercent(keylinePercent); 469 mActionsGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); 470 if (!mButtonActions) { 471 mSubActionsGridView = (VerticalGridView) mMainView.findViewById( 472 R.id.guidedactions_sub_list); 473 mSubActionsBackground = mMainView.findViewById( 474 R.id.guidedactions_sub_list_background); 475 } 476 } 477 mActionsGridView.setFocusable(false); 478 mActionsGridView.setFocusableInTouchMode(false); 479 480 // Cache widths, chevron alpha values, max and min text lines, etc 481 Context ctx = mMainView.getContext(); 482 TypedValue val = new TypedValue(); 483 mEnabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionEnabledChevronAlpha); 484 mDisabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionDisabledChevronAlpha); 485 mTitleMinLines = getInteger(ctx, val, R.attr.guidedActionTitleMinLines); 486 mTitleMaxLines = getInteger(ctx, val, R.attr.guidedActionTitleMaxLines); 487 mDescriptionMinLines = getInteger(ctx, val, R.attr.guidedActionDescriptionMinLines); 488 mVerticalPadding = getDimension(ctx, val, R.attr.guidedActionVerticalPadding); 489 mDisplayHeight = ((WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE)) 490 .getDefaultDisplay().getHeight(); 491 492 mEnabledTextAlpha = getFloatValue(ctx.getResources(), val, R.dimen 493 .lb_guidedactions_item_unselected_text_alpha); 494 mDisabledTextAlpha = getFloatValue(ctx.getResources(), val, R.dimen 495 .lb_guidedactions_item_disabled_text_alpha); 496 mEnabledDescriptionAlpha = getFloatValue(ctx.getResources(), val, R.dimen 497 .lb_guidedactions_item_unselected_description_text_alpha); 498 mDisabledDescriptionAlpha = getFloatValue(ctx.getResources(), val, R.dimen 499 .lb_guidedactions_item_disabled_description_text_alpha); 500 501 mKeyLinePercent = GuidanceStylingRelativeLayout.getKeyLinePercent(ctx); 502 if (mContentView instanceof GuidedActionsRelativeLayout) { 503 ((GuidedActionsRelativeLayout) mContentView).setInterceptKeyEventListener( 504 new GuidedActionsRelativeLayout.InterceptKeyEventListener() { 505 @Override 506 public boolean onInterceptKeyEvent(KeyEvent event) { 507 if (event.getKeyCode() == KeyEvent.KEYCODE_BACK 508 && event.getAction() == KeyEvent.ACTION_UP 509 && mExpandedAction != null) { 510 if ((mExpandedAction.hasSubActions() 511 && isBackKeyToCollapseSubActions()) 512 || (mExpandedAction.hasEditableActivatorView() 513 && isBackKeyToCollapseActivatorView())) { 514 collapseAction(true); 515 return true; 516 } 517 } 518 return false; 519 } 520 } 521 ); 522 } 523 return mMainView; 524 } 525 526 /** 527 * Choose the layout resource for button actions in {@link #onProvideLayoutId()}. 528 */ setAsButtonActions()529 public void setAsButtonActions() { 530 if (mMainView != null) { 531 throw new IllegalStateException("setAsButtonActions() must be called before creating " 532 + "views"); 533 } 534 mButtonActions = true; 535 } 536 537 /** 538 * Returns true if it is button actions list, false for normal actions list. 539 * @return True if it is button actions list, false for normal actions list. 540 */ isButtonActions()541 public boolean isButtonActions() { 542 return mButtonActions; 543 } 544 545 /** 546 * Called when destroy the View created by GuidedActionsStylist. 547 */ onDestroyView()548 public void onDestroyView() { 549 mExpandedAction = null; 550 mExpandTransition = null; 551 mActionsGridView = null; 552 mSubActionsGridView = null; 553 mSubActionsBackground = null; 554 mContentView = null; 555 mMainView = null; 556 } 557 558 /** 559 * Returns the VerticalGridView that displays the list of GuidedActions. 560 * @return The VerticalGridView for this presenter. 561 */ getActionsGridView()562 public @Nullable VerticalGridView getActionsGridView() { 563 return mActionsGridView; 564 } 565 566 /** 567 * Returns the VerticalGridView that displays the sub actions list of an expanded action. 568 * @return The VerticalGridView that displays the sub actions list of an expanded action. 569 */ getSubActionsGridView()570 public @Nullable VerticalGridView getSubActionsGridView() { 571 return mSubActionsGridView; 572 } 573 574 /** 575 * Provides the resource ID of the layout defining the host view for the list of guided actions. 576 * Subclasses may override to provide their own customized layouts. The base implementation 577 * returns {@link androidx.leanback.R.layout#lb_guidedactions} or 578 * {@link androidx.leanback.R.layout#lb_guidedbuttonactions} if 579 * {@link #isButtonActions()} is true. If overridden, the substituted layout should contain 580 * matching IDs for any views that should be managed by the base class; this can be achieved by 581 * starting with a copy of the base layout file. 582 * 583 * @return The resource ID of the layout to be inflated to define the host view for the list of 584 * GuidedActions. 585 */ onProvideLayoutId()586 public int onProvideLayoutId() { 587 return mButtonActions ? R.layout.lb_guidedbuttonactions : R.layout.lb_guidedactions; 588 } 589 590 /** 591 * Return view type of action, each different type can have differently associated layout Id. 592 * Default implementation returns {@link #VIEW_TYPE_DEFAULT}. 593 * @param action The action object. 594 * @return View type that used in {@link #onProvideItemLayoutId(int)}. 595 */ getItemViewType(@onNull GuidedAction action)596 public int getItemViewType(@NonNull GuidedAction action) { 597 if (action instanceof GuidedDatePickerAction) { 598 return VIEW_TYPE_DATE_PICKER; 599 } 600 return VIEW_TYPE_DEFAULT; 601 } 602 603 /** 604 * Provides the resource ID of the layout defining the view for an individual guided actions. 605 * Subclasses may override to provide their own customized layouts. The base implementation 606 * returns {@link androidx.leanback.R.layout#lb_guidedactions_item}. If overridden, 607 * the substituted layout should contain matching IDs for any views that should be managed by 608 * the base class; this can be achieved by starting with a copy of the base layout file. Note 609 * that in order for the item to support editing, the title view should both subclass {@link 610 * android.widget.EditText} and implement {@link ImeKeyMonitor}, 611 * {@link GuidedActionAutofillSupport}; see {@link 612 * GuidedActionEditText}. To support different types of Layouts, override {@link 613 * #onProvideItemLayoutId(int)}. 614 * @return The resource ID of the layout to be inflated to define the view to display an 615 * individual GuidedAction. 616 */ onProvideItemLayoutId()617 public int onProvideItemLayoutId() { 618 return R.layout.lb_guidedactions_item; 619 } 620 621 /** 622 * Provides the resource ID of the layout defining the view for an individual guided actions. 623 * Subclasses may override to provide their own customized layouts. The base implementation 624 * supports: 625 * <ul> 626 * <li>{@link androidx.leanback.R.layout#lb_guidedactions_item}</li> 627 * <li>{{@link androidx.leanback.R.layout#lb_guidedactions_datepicker_item}.</li> 628 * </ul> 629 * If overridden, the substituted layout should contain matching IDs for any views that should 630 * be managed by the base class; this can be achieved by starting with a copy of the base layout 631 * file. Note that in order for the item to support editing, the title view should both subclass 632 * {@link android.widget.EditText} and implement {@link ImeKeyMonitor}; see 633 * {@link GuidedActionEditText}. 634 * 635 * @param viewType View type returned by {@link #getItemViewType(GuidedAction)} 636 * @return The resource ID of the layout to be inflated to define the view to display an 637 * individual GuidedAction. 638 */ onProvideItemLayoutId(int viewType)639 public int onProvideItemLayoutId(int viewType) { 640 if (viewType == VIEW_TYPE_DEFAULT) { 641 return onProvideItemLayoutId(); 642 } else if (viewType == VIEW_TYPE_DATE_PICKER) { 643 return R.layout.lb_guidedactions_datepicker_item; 644 } else { 645 throw new RuntimeException("ViewType " + viewType 646 + " not supported in GuidedActionsStylist"); 647 } 648 } 649 650 /** 651 * Constructs a {@link ViewHolder} capable of representing {@link GuidedAction}s. Subclasses 652 * may choose to return a subclass of ViewHolder. To support different view types, override 653 * {@link #onCreateViewHolder(ViewGroup, int)} 654 * <p> 655 * <i>Note: Should not actually add the created view to the parent; the caller will do 656 * this.</i> 657 * @param parent The view group to be used as the parent of the new view. 658 * @return The view to be added to the caller's view hierarchy. 659 */ onCreateViewHolder(@onNull ViewGroup parent)660 public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent) { 661 LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 662 View v = inflater.inflate(onProvideItemLayoutId(), parent, false); 663 return new ViewHolder(v, parent == mSubActionsGridView); 664 } 665 666 /** 667 * Constructs a {@link ViewHolder} capable of representing {@link GuidedAction}s. Subclasses 668 * may choose to return a subclass of ViewHolder. 669 * <p> 670 * <i>Note: Should not actually add the created view to the parent; the caller will do 671 * this.</i> 672 * @param parent The view group to be used as the parent of the new view. 673 * @param viewType The viewType returned by {@link #getItemViewType(GuidedAction)} 674 * @return The view to be added to the caller's view hierarchy. 675 */ onCreateViewHolder(@onNull ViewGroup parent, int viewType)676 public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 677 if (viewType == VIEW_TYPE_DEFAULT) { 678 return onCreateViewHolder(parent); 679 } 680 LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 681 View v = inflater.inflate(onProvideItemLayoutId(viewType), parent, false); 682 return new ViewHolder(v, parent == mSubActionsGridView); 683 } 684 685 /** 686 * Binds a {@link ViewHolder} to a particular {@link GuidedAction}. 687 * @param vh The view holder to be associated with the given action. 688 * @param action The guided action to be displayed by the view holder's view. 689 * @return The view to be added to the caller's view hierarchy. 690 */ onBindViewHolder(@onNull ViewHolder vh, @NonNull GuidedAction action)691 public void onBindViewHolder(@NonNull ViewHolder vh, @NonNull GuidedAction action) { 692 vh.mAction = action; 693 if (vh.mTitleView != null) { 694 vh.mTitleView.setInputType(action.getInputType()); 695 vh.mTitleView.setText(action.getTitle()); 696 vh.mTitleView.setAlpha(action.isEnabled() ? mEnabledTextAlpha : mDisabledTextAlpha); 697 vh.mTitleView.setFocusable(false); 698 vh.mTitleView.setClickable(false); 699 vh.mTitleView.setLongClickable(false); 700 if (Build.VERSION.SDK_INT >= 28) { 701 if (action.isEditable()) { 702 Api26Impl.setAutofillHints(vh.mTitleView, action.getAutofillHints()); 703 } else { 704 Api26Impl.setAutofillHints(vh.mTitleView, (String[]) null); 705 } 706 } else if (VERSION.SDK_INT >= 26) { 707 // disable autofill below P as dpad/keyboard is not supported 708 Api26Impl.setImportantForAutofill(vh.mTitleView, View.IMPORTANT_FOR_AUTOFILL_NO); 709 } 710 } 711 if (vh.mDescriptionView != null) { 712 vh.mDescriptionView.setInputType(action.getDescriptionInputType()); 713 vh.mDescriptionView.setText(action.getDescription()); 714 vh.mDescriptionView.setVisibility(TextUtils.isEmpty(action.getDescription()) 715 ? View.GONE : View.VISIBLE); 716 vh.mDescriptionView.setAlpha(action.isEnabled() ? mEnabledDescriptionAlpha : 717 mDisabledDescriptionAlpha); 718 vh.mDescriptionView.setFocusable(false); 719 vh.mDescriptionView.setClickable(false); 720 vh.mDescriptionView.setLongClickable(false); 721 if (Build.VERSION.SDK_INT >= 28) { 722 if (action.isDescriptionEditable()) { 723 Api26Impl.setAutofillHints(vh.mDescriptionView, action.getAutofillHints()); 724 } else { 725 Api26Impl.setAutofillHints(vh.mDescriptionView, (String[]) null); 726 } 727 } else if (VERSION.SDK_INT >= 26) { 728 // disable autofill below P as dpad/keyboard is not supported 729 Api26Impl.setImportantForAutofill(vh.mTitleView, View.IMPORTANT_FOR_AUTOFILL_NO); 730 } 731 } 732 // Clients might want the check mark view to be gone entirely, in which case, ignore it. 733 if (vh.mCheckmarkView != null) { 734 onBindCheckMarkView(vh, action); 735 } 736 setIcon(vh.mIconView, action); 737 738 if (action.hasMultilineDescription()) { 739 if (vh.mTitleView != null) { 740 setMaxLines(vh.mTitleView, mTitleMaxLines); 741 vh.mTitleView.setInputType( 742 vh.mTitleView.getInputType() | InputType.TYPE_TEXT_FLAG_MULTI_LINE); 743 if (vh.mDescriptionView != null) { 744 vh.mDescriptionView.setInputType(vh.mDescriptionView.getInputType() 745 | InputType.TYPE_TEXT_FLAG_MULTI_LINE); 746 vh.mDescriptionView.setMaxHeight(getDescriptionMaxHeight(vh.mTitleView)); 747 } 748 } 749 } else { 750 if (vh.mTitleView != null) { 751 setMaxLines(vh.mTitleView, mTitleMinLines); 752 } 753 if (vh.mDescriptionView != null) { 754 setMaxLines(vh.mDescriptionView, mDescriptionMinLines); 755 } 756 } 757 if (vh.mActivatorView != null) { 758 onBindActivatorView(vh, action); 759 } 760 setEditingMode(vh, false /*editing*/, false /*withTransition*/); 761 if (action.isFocusable()) { 762 vh.itemView.setFocusable(true); 763 ((ViewGroup) vh.itemView).setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); 764 } else { 765 vh.itemView.setFocusable(false); 766 ((ViewGroup) vh.itemView).setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); 767 } 768 setupImeOptions(vh, action); 769 770 updateChevronAndVisibility(vh); 771 } 772 773 /** 774 * Switches action to edit mode and pops up the keyboard. 775 */ openInEditMode(@onNull GuidedAction action)776 public void openInEditMode(@NonNull GuidedAction action) { 777 final GuidedActionAdapter guidedActionAdapter = 778 (GuidedActionAdapter) getActionsGridView().getAdapter(); 779 int actionIndex = guidedActionAdapter.getActions().indexOf(action); 780 if (actionIndex < 0 || !action.isEditable()) { 781 return; 782 } 783 784 getActionsGridView().setSelectedPosition(actionIndex, new ViewHolderTask() { 785 @Override 786 public void run(RecyclerView.@NonNull ViewHolder viewHolder) { 787 ViewHolder vh = (ViewHolder) viewHolder; 788 guidedActionAdapter.mGroup.openIme(guidedActionAdapter, vh); 789 } 790 }); 791 } 792 setMaxLines(TextView view, int maxLines)793 private static void setMaxLines(TextView view, int maxLines) { 794 // setSingleLine must be called before setMaxLines because it resets maximum to 795 // Integer.MAX_VALUE. 796 if (maxLines == 1) { 797 view.setSingleLine(true); 798 } else { 799 view.setSingleLine(false); 800 view.setMaxLines(maxLines); 801 } 802 } 803 804 /** 805 * Called by {@link #onBindViewHolder(ViewHolder, GuidedAction)} to setup IME options. Default 806 * implementation assigns {@link EditorInfo#IME_ACTION_DONE}. Subclass may override. 807 * @param vh The view holder to be associated with the given action. 808 * @param action The guided action to be displayed by the view holder's view. 809 */ setupImeOptions(@onNull ViewHolder vh, @NonNull GuidedAction action)810 protected void setupImeOptions(@NonNull ViewHolder vh, @NonNull GuidedAction action) { 811 setupNextImeOptions(vh.getEditableTitleView()); 812 setupNextImeOptions(vh.getEditableDescriptionView()); 813 } 814 setupNextImeOptions(EditText edit)815 private void setupNextImeOptions(EditText edit) { 816 if (edit != null) { 817 edit.setImeOptions(EditorInfo.IME_ACTION_NEXT); 818 } 819 } 820 821 /** 822 * @deprecated This method is for internal library use only and should not 823 * be called directly. 824 */ 825 @Deprecated setEditingMode(ViewHolder vh, GuidedAction action, boolean editing)826 public void setEditingMode(ViewHolder vh, GuidedAction action, boolean editing) { 827 if (editing != vh.isInEditing() && isInExpandTransition()) { 828 onEditingModeChange(vh, action, editing); 829 } 830 } 831 setEditingMode(ViewHolder vh, boolean editing)832 void setEditingMode(ViewHolder vh, boolean editing) { 833 setEditingMode(vh, editing, true /*withTransition*/); 834 } 835 setEditingMode(ViewHolder vh, boolean editing, boolean withTransition)836 void setEditingMode(ViewHolder vh, boolean editing, boolean withTransition) { 837 if (editing != vh.isInEditing() && !isInExpandTransition()) { 838 onEditingModeChange(vh, editing, withTransition); 839 } 840 } 841 842 /** 843 * @deprecated Use {@link #onEditingModeChange(ViewHolder, boolean, boolean)}. 844 */ 845 @Deprecated onEditingModeChange(ViewHolder vh, GuidedAction action, boolean editing)846 protected void onEditingModeChange(ViewHolder vh, GuidedAction action, boolean editing) { 847 } 848 849 /** 850 * Called when editing mode of an ViewHolder is changed. Subclass must call 851 * <code>super.onEditingModeChange(vh,editing,withTransition)</code>. 852 * 853 * @param vh ViewHolder to change editing mode. 854 * @param editing True to enable editing, false to stop editing 855 * @param withTransition True to run expand transiiton, false otherwise. 856 */ 857 @CallSuper onEditingModeChange( @onNull ViewHolder vh, boolean editing, boolean withTransition)858 protected void onEditingModeChange( 859 @NonNull ViewHolder vh, 860 boolean editing, 861 boolean withTransition) { 862 GuidedAction action = vh.getAction(); 863 TextView titleView = vh.getTitleView(); 864 TextView descriptionView = vh.getDescriptionView(); 865 if (editing) { 866 CharSequence editTitle = action.getEditTitle(); 867 if (titleView != null && editTitle != null) { 868 titleView.setText(editTitle); 869 } 870 CharSequence editDescription = action.getEditDescription(); 871 if (descriptionView != null && editDescription != null) { 872 descriptionView.setText(editDescription); 873 } 874 if (action.isDescriptionEditable()) { 875 if (descriptionView != null) { 876 descriptionView.setVisibility(View.VISIBLE); 877 descriptionView.setInputType(action.getDescriptionEditInputType()); 878 descriptionView.requestFocusFromTouch(); 879 } 880 vh.mEditingMode = EDITING_DESCRIPTION; 881 } else if (action.isEditable()){ 882 if (titleView != null) { 883 titleView.setInputType(action.getEditInputType()); 884 titleView.requestFocusFromTouch(); 885 } 886 vh.mEditingMode = EDITING_TITLE; 887 } else if (vh.mActivatorView != null) { 888 onEditActivatorView(vh, editing, withTransition); 889 vh.mEditingMode = EDITING_ACTIVATOR_VIEW; 890 } 891 } else { 892 if (titleView != null) { 893 titleView.setText(action.getTitle()); 894 } 895 if (descriptionView != null) { 896 descriptionView.setText(action.getDescription()); 897 } 898 if (vh.mEditingMode == EDITING_DESCRIPTION) { 899 if (descriptionView != null) { 900 descriptionView.setVisibility(TextUtils.isEmpty(action.getDescription()) 901 ? View.GONE : View.VISIBLE); 902 descriptionView.setInputType(action.getDescriptionInputType()); 903 } 904 } else if (vh.mEditingMode == EDITING_TITLE) { 905 if (titleView != null) { 906 titleView.setInputType(action.getInputType()); 907 } 908 } else if (vh.mEditingMode == EDITING_ACTIVATOR_VIEW) { 909 if (vh.mActivatorView != null) { 910 onEditActivatorView(vh, editing, withTransition); 911 } 912 } 913 vh.mEditingMode = EDITING_NONE; 914 } 915 // call deprecated method for backward compatible 916 onEditingModeChange(vh, action, editing); 917 } 918 919 /** 920 * Animates the view holder's view (or subviews thereof) when the action has had its focus 921 * state changed. 922 * @param vh The view holder associated with the relevant action. 923 * @param focused True if the action has become focused, false if it has lost focus. 924 */ onAnimateItemFocused(@onNull ViewHolder vh, boolean focused)925 public void onAnimateItemFocused(@NonNull ViewHolder vh, boolean focused) { 926 // No animations for this, currently, because the animation is done on 927 // mSelectorView 928 } 929 930 /** 931 * Animates the view holder's view (or subviews thereof) when the action has had its press 932 * state changed. 933 * @param vh The view holder associated with the relevant action. 934 * @param pressed True if the action has been pressed, false if it has been unpressed. 935 */ onAnimateItemPressed(@onNull ViewHolder vh, boolean pressed)936 public void onAnimateItemPressed(@NonNull ViewHolder vh, boolean pressed) { 937 vh.press(pressed); 938 } 939 940 /** 941 * Resets the view holder's view to unpressed state. 942 * @param vh The view holder associated with the relevant action. 943 */ onAnimateItemPressedCancelled(@onNull ViewHolder vh)944 public void onAnimateItemPressedCancelled(@NonNull ViewHolder vh) { 945 vh.press(false); 946 } 947 948 /** 949 * Animates the view holder's view (or subviews thereof) when the action has had its check state 950 * changed. Default implementation calls setChecked() if {@link ViewHolder#getCheckmarkView()} 951 * is instance of {@link Checkable}. 952 * 953 * @param vh The view holder associated with the relevant action. 954 * @param checked True if the action has become checked, false if it has become unchecked. 955 * @see #onBindCheckMarkView(ViewHolder, GuidedAction) 956 */ onAnimateItemChecked(@onNull ViewHolder vh, boolean checked)957 public void onAnimateItemChecked(@NonNull ViewHolder vh, boolean checked) { 958 if (vh.mCheckmarkView instanceof Checkable) { 959 ((Checkable) vh.mCheckmarkView).setChecked(checked); 960 } 961 } 962 963 /** 964 * Sets states of check mark view, called by {@link #onBindViewHolder(ViewHolder, GuidedAction)} 965 * when action's checkset Id is other than {@link GuidedAction#NO_CHECK_SET}. Default 966 * implementation assigns drawable loaded from theme attribute 967 * {@link android.R.attr#listChoiceIndicatorMultiple} for checkbox or 968 * {@link android.R.attr#listChoiceIndicatorSingle} for radio button. Subclass rarely needs 969 * override the method, instead app can provide its own drawable that supports transition 970 * animations, change theme attributes {@link android.R.attr#listChoiceIndicatorMultiple} and 971 * {@link android.R.attr#listChoiceIndicatorSingle} in {androidx.leanback.R. 972 * styleable#LeanbackGuidedStepTheme}. 973 * 974 * @param vh The view holder associated with the relevant action. 975 * @param action The GuidedAction object to bind to. 976 * @see #onAnimateItemChecked(ViewHolder, boolean) 977 */ onBindCheckMarkView(@onNull ViewHolder vh, @NonNull GuidedAction action)978 public void onBindCheckMarkView(@NonNull ViewHolder vh, @NonNull GuidedAction action) { 979 if (action.getCheckSetId() != GuidedAction.NO_CHECK_SET) { 980 vh.mCheckmarkView.setVisibility(View.VISIBLE); 981 int attrId = action.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID 982 ? android.R.attr.listChoiceIndicatorMultiple 983 : android.R.attr.listChoiceIndicatorSingle; 984 final Context context = vh.mCheckmarkView.getContext(); 985 Drawable drawable = null; 986 TypedValue typedValue = new TypedValue(); 987 if (context.getTheme().resolveAttribute(attrId, typedValue, true)) { 988 drawable = ContextCompat.getDrawable(context, typedValue.resourceId); 989 } 990 vh.mCheckmarkView.setImageDrawable(drawable); 991 if (vh.mCheckmarkView instanceof Checkable) { 992 ((Checkable) vh.mCheckmarkView).setChecked(action.isChecked()); 993 } 994 } else { 995 vh.mCheckmarkView.setVisibility(View.GONE); 996 } 997 } 998 999 /** 1000 * Performs binding activator view value to action. Default implementation supports 1001 * GuidedDatePickerAction, subclass may override to add support of other views. 1002 * @param vh ViewHolder of activator view. 1003 * @param action GuidedAction to bind. 1004 */ onBindActivatorView(@onNull ViewHolder vh, @NonNull GuidedAction action)1005 public void onBindActivatorView(@NonNull ViewHolder vh, @NonNull GuidedAction action) { 1006 if (action instanceof GuidedDatePickerAction) { 1007 GuidedDatePickerAction dateAction = (GuidedDatePickerAction) action; 1008 DatePicker dateView = (DatePicker) vh.mActivatorView; 1009 dateView.setDatePickerFormat(dateAction.getDatePickerFormat()); 1010 if (dateAction.getMinDate() != Long.MIN_VALUE) { 1011 dateView.setMinDate(dateAction.getMinDate()); 1012 } 1013 if (dateAction.getMaxDate() != Long.MAX_VALUE) { 1014 dateView.setMaxDate(dateAction.getMaxDate()); 1015 } 1016 Calendar c = Calendar.getInstance(); 1017 c.setTimeInMillis(dateAction.getDate()); 1018 dateView.setDate(c.get(Calendar.YEAR), c.get(Calendar.MONTH), 1019 c.get(Calendar.DAY_OF_MONTH), false); 1020 } 1021 } 1022 1023 /** 1024 * Performs updating GuidedAction from activator view. Default implementation supports 1025 * GuidedDatePickerAction, subclass may override to add support of other views. 1026 * @param vh ViewHolder of activator view. 1027 * @param action GuidedAction to update. 1028 * @return True if value has been updated, false otherwise. 1029 */ onUpdateActivatorView(@onNull ViewHolder vh, @NonNull GuidedAction action)1030 public boolean onUpdateActivatorView(@NonNull ViewHolder vh, @NonNull GuidedAction action) { 1031 if (action instanceof GuidedDatePickerAction) { 1032 GuidedDatePickerAction dateAction = (GuidedDatePickerAction) action; 1033 DatePicker dateView = (DatePicker) vh.mActivatorView; 1034 if (dateAction.getDate() != dateView.getDate()) { 1035 dateAction.setDate(dateView.getDate()); 1036 return true; 1037 } 1038 } 1039 return false; 1040 } 1041 1042 /** 1043 * Sets listener for reporting view being edited. 1044 */ 1045 @RestrictTo(LIBRARY_GROUP_PREFIX) setEditListener(@onNull EditListener listener)1046 public void setEditListener(@NonNull EditListener listener) { 1047 mEditListener = listener; 1048 } 1049 onEditActivatorView(final ViewHolder vh, boolean editing, final boolean withTransition)1050 void onEditActivatorView(final ViewHolder vh, boolean editing, final boolean withTransition) { 1051 if (editing) { 1052 startExpanded(vh, withTransition); 1053 vh.itemView.setFocusable(false); 1054 vh.mActivatorView.requestFocus(); 1055 vh.mActivatorView.setOnClickListener(new View.OnClickListener() { 1056 @Override 1057 public void onClick(View v) { 1058 if (!isInExpandTransition()) { 1059 ((GuidedActionAdapter) getActionsGridView().getAdapter()) 1060 .performOnActionClick(vh); 1061 } 1062 } 1063 }); 1064 } else { 1065 if (onUpdateActivatorView(vh, vh.getAction())) { 1066 if (mEditListener != null) { 1067 mEditListener.onGuidedActionEditedAndProceed(vh.getAction()); 1068 } 1069 } 1070 vh.itemView.setFocusable(true); 1071 vh.itemView.requestFocus(); 1072 startExpanded(null, withTransition); 1073 vh.mActivatorView.setOnClickListener(null); 1074 vh.mActivatorView.setClickable(false); 1075 } 1076 } 1077 1078 /** 1079 * Sets states of chevron view, called by {@link #onBindViewHolder(ViewHolder, GuidedAction)}. 1080 * Subclass may override. 1081 * 1082 * @param vh The view holder associated with the relevant action. 1083 * @param action The GuidedAction object to bind to. 1084 */ onBindChevronView(@onNull ViewHolder vh, @NonNull GuidedAction action)1085 public void onBindChevronView(@NonNull ViewHolder vh, @NonNull GuidedAction action) { 1086 final boolean hasNext = action.hasNext(); 1087 final boolean hasSubActions = action.hasSubActions(); 1088 if (hasNext || hasSubActions) { 1089 vh.mChevronView.setVisibility(View.VISIBLE); 1090 vh.mChevronView.setAlpha(action.isEnabled() ? mEnabledChevronAlpha : 1091 mDisabledChevronAlpha); 1092 if (hasNext) { 1093 float r = mMainView != null 1094 && mMainView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? 180f : 0f; 1095 vh.mChevronView.setRotation(r); 1096 } else if (action == mExpandedAction) { 1097 vh.mChevronView.setRotation(270); 1098 } else { 1099 vh.mChevronView.setRotation(90); 1100 } 1101 } else { 1102 vh.mChevronView.setVisibility(View.GONE); 1103 1104 } 1105 } 1106 1107 /** 1108 * Expands or collapse the sub actions list view with transition animation 1109 * @param avh When not null, fill sub actions list of this ViewHolder into sub actions list and 1110 * hide the other items in main list. When null, collapse the sub actions list. 1111 * @deprecated use {@link #expandAction(GuidedAction, boolean)} and 1112 * {@link #collapseAction(boolean)} 1113 */ 1114 @Deprecated setExpandedViewHolder(ViewHolder avh)1115 public void setExpandedViewHolder(ViewHolder avh) { 1116 expandAction(avh == null ? null : avh.getAction(), isExpandTransitionSupported()); 1117 } 1118 1119 /** 1120 * Returns true if it is running an expanding or collapsing transition, false otherwise. 1121 * @return True if it is running an expanding or collapsing transition, false otherwise. 1122 */ isInExpandTransition()1123 public boolean isInExpandTransition() { 1124 return mExpandTransition != null; 1125 } 1126 1127 /** 1128 * Returns if expand/collapse animation is supported. When this method returns true, 1129 * {@link #startExpandedTransition(ViewHolder)} will be used. When this method returns false, 1130 * {@link #onUpdateExpandedViewHolder(ViewHolder)} will be called. 1131 * @return True if it is running an expanding or collapsing transition, false otherwise. 1132 */ isExpandTransitionSupported()1133 public boolean isExpandTransitionSupported() { 1134 return VERSION.SDK_INT >= 21; 1135 } 1136 1137 /** 1138 * Start transition to expand or collapse GuidedActionStylist. 1139 * @param avh When not null, the GuidedActionStylist expands the sub actions of avh. When null 1140 * the GuidedActionStylist will collapse sub actions. 1141 * @deprecated use {@link #expandAction(GuidedAction, boolean)} and 1142 * {@link #collapseAction(boolean)} 1143 */ 1144 @Deprecated startExpandedTransition(ViewHolder avh)1145 public void startExpandedTransition(ViewHolder avh) { 1146 expandAction(avh == null ? null : avh.getAction(), isExpandTransitionSupported()); 1147 } 1148 1149 /** 1150 * Enable or disable using BACK key to collapse sub actions list. Default is enabled. 1151 * 1152 * @param backToCollapse True to enable using BACK key to collapse sub actions list, false 1153 * to disable. 1154 * @see GuidedAction#hasSubActions 1155 * @see GuidedAction#getSubActions 1156 */ setBackKeyToCollapseSubActions(boolean backToCollapse)1157 public final void setBackKeyToCollapseSubActions(boolean backToCollapse) { 1158 mBackToCollapseSubActions = backToCollapse; 1159 } 1160 1161 /** 1162 * @return True if using BACK key to collapse sub actions list, false otherwise. Default value 1163 * is true. 1164 * 1165 * @see GuidedAction#hasSubActions 1166 * @see GuidedAction#getSubActions 1167 */ isBackKeyToCollapseSubActions()1168 public final boolean isBackKeyToCollapseSubActions() { 1169 return mBackToCollapseSubActions; 1170 } 1171 1172 /** 1173 * Enable or disable using BACK key to collapse {@link GuidedAction} with editable activator 1174 * view. Default is enabled. 1175 * 1176 * @param backToCollapse True to enable using BACK key to collapse {@link GuidedAction} with 1177 * editable activator view. 1178 * @see GuidedAction#hasEditableActivatorView 1179 */ setBackKeyToCollapseActivatorView(boolean backToCollapse)1180 public final void setBackKeyToCollapseActivatorView(boolean backToCollapse) { 1181 mBackToCollapseActivatorView = backToCollapse; 1182 } 1183 1184 /** 1185 * @return True if using BACK key to collapse {@link GuidedAction} with editable activator 1186 * view, false otherwise. Default value is true. 1187 * 1188 * @see GuidedAction#hasEditableActivatorView 1189 */ isBackKeyToCollapseActivatorView()1190 public final boolean isBackKeyToCollapseActivatorView() { 1191 return mBackToCollapseActivatorView; 1192 } 1193 1194 /** 1195 * Expand an action. Do nothing if it is in animation or there is action expanded. 1196 * 1197 * @param action Action to expand. 1198 * @param withTransition True to run transition animation, false otherwsie. 1199 */ expandAction(@onNull GuidedAction action, final boolean withTransition)1200 public void expandAction(@NonNull GuidedAction action, final boolean withTransition) { 1201 if (isInExpandTransition() || mExpandedAction != null) { 1202 return; 1203 } 1204 int actionPosition = 1205 ((GuidedActionAdapter) getActionsGridView().getAdapter()).indexOf(action); 1206 if (actionPosition < 0) { 1207 return; 1208 } 1209 boolean runTransition = isExpandTransitionSupported() && withTransition; 1210 if (!runTransition) { 1211 getActionsGridView().setSelectedPosition(actionPosition, 1212 new ViewHolderTask() { 1213 @Override 1214 public void run(RecyclerView.ViewHolder vh) { 1215 GuidedActionsStylist.ViewHolder avh = 1216 (GuidedActionsStylist.ViewHolder)vh; 1217 if (avh.getAction().hasEditableActivatorView()) { 1218 setEditingMode(avh, true /*editing*/, false /*withTransition*/); 1219 } else { 1220 onUpdateExpandedViewHolder(avh); 1221 } 1222 } 1223 }); 1224 if (action.hasSubActions()) { 1225 onUpdateSubActionsGridView(action, true); 1226 } 1227 } else { 1228 getActionsGridView().setSelectedPosition(actionPosition, 1229 new ViewHolderTask() { 1230 @Override 1231 public void run(RecyclerView.ViewHolder vh) { 1232 GuidedActionsStylist.ViewHolder avh = 1233 (GuidedActionsStylist.ViewHolder)vh; 1234 if (avh.getAction().hasEditableActivatorView()) { 1235 setEditingMode(avh, true /*editing*/, true /*withTransition*/); 1236 } else { 1237 startExpanded(avh, true); 1238 } 1239 } 1240 }); 1241 } 1242 1243 } 1244 1245 /** 1246 * Collapse expanded action. Do nothing if it is in animation or there is no action expanded. 1247 * 1248 * @param withTransition True to run transition animation, false otherwsie. 1249 */ collapseAction(boolean withTransition)1250 public void collapseAction(boolean withTransition) { 1251 if (isInExpandTransition() || mExpandedAction == null) { 1252 return; 1253 } 1254 boolean runTransition = isExpandTransitionSupported() && withTransition; 1255 int actionPosition = 1256 ((GuidedActionAdapter) getActionsGridView().getAdapter()).indexOf(mExpandedAction); 1257 if (actionPosition < 0) { 1258 return; 1259 } 1260 if (mExpandedAction.hasEditableActivatorView()) { 1261 setEditingMode( 1262 ((ViewHolder) getActionsGridView().findViewHolderForPosition(actionPosition)), 1263 false /*editing*/, 1264 runTransition); 1265 } else { 1266 startExpanded(null, runTransition); 1267 } 1268 } 1269 getKeyLine()1270 int getKeyLine() { 1271 return (int) (mKeyLinePercent * mActionsGridView.getHeight() / 100); 1272 } 1273 1274 /** 1275 * Internal method with assumption we already scroll to the new ViewHolder or is currently 1276 * expanded. 1277 */ startExpanded(ViewHolder avh, final boolean withTransition)1278 void startExpanded(ViewHolder avh, final boolean withTransition) { 1279 ViewHolder focusAvh = null; // expand / collapse view holder 1280 final int count = mActionsGridView.getChildCount(); 1281 for (int i = 0; i < count; i++) { 1282 ViewHolder vh = (ViewHolder) mActionsGridView 1283 .getChildViewHolder(mActionsGridView.getChildAt(i)); 1284 if (avh == null && vh.itemView.getVisibility() == View.VISIBLE) { 1285 // going to collapse this one. 1286 focusAvh = vh; 1287 break; 1288 } else if (avh != null && vh.getAction() == avh.getAction()) { 1289 // going to expand this one. 1290 focusAvh = vh; 1291 break; 1292 } 1293 } 1294 if (focusAvh == null) { 1295 // huh? 1296 return; 1297 } 1298 boolean isExpand = avh != null; 1299 boolean isSubActionTransition = focusAvh.getAction().hasSubActions(); 1300 if (withTransition) { 1301 Object set = TransitionHelper.createTransitionSet(false); 1302 float slideDistance = isSubActionTransition ? focusAvh.itemView.getHeight() 1303 : focusAvh.itemView.getHeight() * 0.5f; 1304 Object slideAndFade = TransitionHelper.createFadeAndShortSlide( 1305 Gravity.TOP | Gravity.BOTTOM, 1306 slideDistance); 1307 TransitionHelper.setEpicenterCallback(slideAndFade, new TransitionEpicenterCallback() { 1308 Rect mRect = new Rect(); 1309 @Override 1310 public Rect onGetEpicenter(Object transition) { 1311 int centerY = getKeyLine(); 1312 int centerX = 0; 1313 mRect.set(centerX, centerY, centerX, centerY); 1314 return mRect; 1315 } 1316 }); 1317 Object changeFocusItemTransform = TransitionHelper.createChangeTransform(); 1318 Object changeFocusItemBounds = TransitionHelper.createChangeBounds(false); 1319 Object fade = TransitionHelper.createFadeTransition(TransitionHelper.FADE_IN 1320 | TransitionHelper.FADE_OUT); 1321 Object changeGridBounds = TransitionHelper.createChangeBounds(false); 1322 if (avh == null) { 1323 TransitionHelper.setStartDelay(slideAndFade, 150); 1324 TransitionHelper.setStartDelay(changeFocusItemTransform, 100); 1325 TransitionHelper.setStartDelay(changeFocusItemBounds, 100); 1326 TransitionHelper.setStartDelay(changeGridBounds, 100); 1327 } else { 1328 TransitionHelper.setStartDelay(fade, 100); 1329 TransitionHelper.setStartDelay(changeGridBounds, 50); 1330 TransitionHelper.setStartDelay(changeFocusItemTransform, 50); 1331 TransitionHelper.setStartDelay(changeFocusItemBounds, 50); 1332 } 1333 for (int i = 0; i < count; i++) { 1334 ViewHolder vh = (ViewHolder) mActionsGridView 1335 .getChildViewHolder(mActionsGridView.getChildAt(i)); 1336 if (vh == focusAvh) { 1337 // going to expand/collapse this one. 1338 if (isSubActionTransition) { 1339 TransitionHelper.include(changeFocusItemTransform, vh.itemView); 1340 TransitionHelper.include(changeFocusItemBounds, vh.itemView); 1341 } 1342 } else { 1343 // going to slide this item to top / bottom. 1344 TransitionHelper.include(slideAndFade, vh.itemView); 1345 TransitionHelper.exclude(fade, vh.itemView, true); 1346 } 1347 } 1348 TransitionHelper.include(changeGridBounds, mSubActionsGridView); 1349 TransitionHelper.include(changeGridBounds, mSubActionsBackground); 1350 TransitionHelper.addTransition(set, slideAndFade); 1351 // note that we don't run ChangeBounds for activating view due to the rounding problem 1352 // of multiple level views ChangeBounds animation causing vertical jittering. 1353 if (isSubActionTransition) { 1354 TransitionHelper.addTransition(set, changeFocusItemTransform); 1355 TransitionHelper.addTransition(set, changeFocusItemBounds); 1356 } 1357 TransitionHelper.addTransition(set, fade); 1358 TransitionHelper.addTransition(set, changeGridBounds); 1359 mExpandTransition = set; 1360 TransitionHelper.addTransitionListener(mExpandTransition, new TransitionListener() { 1361 @Override 1362 public void onTransitionEnd(Object transition) { 1363 mExpandTransition = null; 1364 } 1365 }); 1366 if (isExpand && isSubActionTransition) { 1367 // To expand sub actions, move original position of sub actions to bottom of item 1368 int startY = avh.itemView.getBottom(); 1369 mSubActionsGridView.offsetTopAndBottom(startY - mSubActionsGridView.getTop()); 1370 mSubActionsBackground.offsetTopAndBottom(startY - mSubActionsBackground.getTop()); 1371 } 1372 TransitionHelper.beginDelayedTransition(mMainView, mExpandTransition); 1373 } 1374 onUpdateExpandedViewHolder(avh); 1375 if (isSubActionTransition) { 1376 onUpdateSubActionsGridView(focusAvh.getAction(), isExpand); 1377 } 1378 } 1379 1380 /** 1381 * @return True if sub actions list is expanded. 1382 */ isSubActionsExpanded()1383 public boolean isSubActionsExpanded() { 1384 return mExpandedAction != null && mExpandedAction.hasSubActions(); 1385 } 1386 1387 /** 1388 * @return True if there is {@link #getExpandedAction()} is not null, false otherwise. 1389 */ isExpanded()1390 public boolean isExpanded() { 1391 return mExpandedAction != null; 1392 } 1393 1394 /** 1395 * @return Current expanded GuidedAction or null if not expanded. 1396 */ getExpandedAction()1397 public @Nullable GuidedAction getExpandedAction() { 1398 return mExpandedAction; 1399 } 1400 1401 /** 1402 * Expand or collapse GuidedActionStylist. 1403 * @param avh When not null, the GuidedActionStylist expands the sub actions of avh. When null 1404 * the GuidedActionStylist will collapse sub actions. 1405 */ onUpdateExpandedViewHolder(@ullable ViewHolder avh)1406 public void onUpdateExpandedViewHolder(@Nullable ViewHolder avh) { 1407 1408 // Note about setting the prune child flag back & forth here: without this, the actions that 1409 // go off the screen from the top or bottom become invisible forever. This is because once 1410 // an action is expanded, it takes more space which in turn kicks out some other actions 1411 // off of the screen. Once, this action is collapsed (after the second click) and the 1412 // visibility flag is set back to true for all existing actions, 1413 // the off-the-screen actions are pruned from the view, thus 1414 // could not be accessed, had we not disabled pruning prior to this. 1415 if (avh == null) { 1416 mExpandedAction = null; 1417 mActionsGridView.setPruneChild(true); 1418 } else if (avh.getAction() != mExpandedAction) { 1419 mExpandedAction = avh.getAction(); 1420 mActionsGridView.setPruneChild(false); 1421 } 1422 // In expanding mode, notifyItemChange on expanded item will reset the translationY by 1423 // the default ItemAnimator. So disable ItemAnimation in expanding mode. 1424 mActionsGridView.setAnimateChildLayout(false); 1425 final int count = mActionsGridView.getChildCount(); 1426 for (int i = 0; i < count; i++) { 1427 ViewHolder vh = (ViewHolder) mActionsGridView 1428 .getChildViewHolder(mActionsGridView.getChildAt(i)); 1429 updateChevronAndVisibility(vh); 1430 } 1431 } 1432 onUpdateSubActionsGridView(GuidedAction action, boolean expand)1433 void onUpdateSubActionsGridView(GuidedAction action, boolean expand) { 1434 if (mSubActionsGridView != null) { 1435 ViewGroup.MarginLayoutParams lp = 1436 (ViewGroup.MarginLayoutParams) mSubActionsGridView.getLayoutParams(); 1437 GuidedActionAdapter adapter = (GuidedActionAdapter) mSubActionsGridView.getAdapter(); 1438 if (expand) { 1439 // set to negative value so GuidedActionRelativeLayout will override with 1440 // keyLine percentage. 1441 lp.topMargin = -2; 1442 lp.height = ViewGroup.MarginLayoutParams.MATCH_PARENT; 1443 mSubActionsGridView.setLayoutParams(lp); 1444 mSubActionsGridView.setVisibility(View.VISIBLE); 1445 mSubActionsBackground.setVisibility(View.VISIBLE); 1446 mSubActionsGridView.requestFocus(); 1447 adapter.setActions(action.getSubActions()); 1448 } else { 1449 // set to explicit value, which will disable the keyLine percentage calculation 1450 // in GuidedRelativeLayout. 1451 int actionPosition = ((GuidedActionAdapter) mActionsGridView.getAdapter()) 1452 .indexOf(action); 1453 lp.topMargin = mActionsGridView.getLayoutManager() 1454 .findViewByPosition(actionPosition).getBottom(); 1455 lp.height = 0; 1456 mSubActionsGridView.setVisibility(View.INVISIBLE); 1457 mSubActionsBackground.setVisibility(View.INVISIBLE); 1458 mSubActionsGridView.setLayoutParams(lp); 1459 adapter.setActions(Collections.<GuidedAction>emptyList()); 1460 mActionsGridView.requestFocus(); 1461 } 1462 } 1463 } 1464 updateChevronAndVisibility(ViewHolder vh)1465 private void updateChevronAndVisibility(ViewHolder vh) { 1466 if (!vh.isSubAction()) { 1467 if (mExpandedAction == null) { 1468 vh.itemView.setVisibility(View.VISIBLE); 1469 vh.itemView.setTranslationY(0); 1470 if (vh.mActivatorView != null) { 1471 vh.setActivated(false); 1472 } 1473 } else if (vh.getAction() == mExpandedAction) { 1474 vh.itemView.setVisibility(View.VISIBLE); 1475 if (vh.getAction().hasSubActions()) { 1476 vh.itemView.setTranslationY(getKeyLine() - vh.itemView.getBottom()); 1477 } else if (vh.mActivatorView != null) { 1478 vh.itemView.setTranslationY(0); 1479 vh.setActivated(true); 1480 } 1481 } else { 1482 vh.itemView.setVisibility(View.INVISIBLE); 1483 vh.itemView.setTranslationY(0); 1484 } 1485 } 1486 if (vh.mChevronView != null) { 1487 onBindChevronView(vh, vh.getAction()); 1488 } 1489 } 1490 1491 /* 1492 * ========================================== 1493 * FragmentAnimationProvider overrides 1494 * ========================================== 1495 */ 1496 1497 /** 1498 * {@inheritDoc} 1499 */ 1500 @Override onImeAppearing(@onNull List<Animator> animators)1501 public void onImeAppearing(@NonNull List<Animator> animators) { 1502 } 1503 1504 /** 1505 * {@inheritDoc} 1506 */ 1507 @Override onImeDisappearing(@onNull List<Animator> animators)1508 public void onImeDisappearing(@NonNull List<Animator> animators) { 1509 } 1510 1511 /* 1512 * ========================================== 1513 * Private methods 1514 * ========================================== 1515 */ 1516 getFloat(Context ctx, TypedValue typedValue, int attrId)1517 private static float getFloat(Context ctx, TypedValue typedValue, int attrId) { 1518 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 1519 return typedValue.getFloat(); 1520 } 1521 getFloatValue(Resources resources, TypedValue typedValue, int resId)1522 private static float getFloatValue(Resources resources, TypedValue typedValue, int resId) { 1523 resources.getValue(resId, typedValue, true); 1524 return typedValue.getFloat(); 1525 } 1526 getInteger(Context ctx, TypedValue typedValue, int attrId)1527 private static int getInteger(Context ctx, TypedValue typedValue, int attrId) { 1528 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 1529 return ctx.getResources().getInteger(typedValue.resourceId); 1530 } 1531 getDimension(Context ctx, TypedValue typedValue, int attrId)1532 private static int getDimension(Context ctx, TypedValue typedValue, int attrId) { 1533 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 1534 return ctx.getResources().getDimensionPixelSize(typedValue.resourceId); 1535 } 1536 setIcon(final ImageView iconView, GuidedAction action)1537 private boolean setIcon(final ImageView iconView, GuidedAction action) { 1538 Drawable icon = null; 1539 if (iconView != null) { 1540 icon = action.getIcon(); 1541 if (icon != null) { 1542 // setImageDrawable resets the drawable's level unless we set the view level first. 1543 iconView.setImageLevel(icon.getLevel()); 1544 iconView.setImageDrawable(icon); 1545 iconView.setVisibility(View.VISIBLE); 1546 } else { 1547 iconView.setVisibility(View.GONE); 1548 } 1549 } 1550 return icon != null; 1551 } 1552 1553 /** 1554 * @return the max height in pixels the description can be such that the 1555 * action nicely takes up the entire screen. 1556 */ getDescriptionMaxHeight(TextView title)1557 private int getDescriptionMaxHeight(TextView title) { 1558 // The 2 multiplier on the title height calculation is a 1559 // conservative estimate for font padding which can not be 1560 // calculated at this stage since the view hasn't been rendered yet. 1561 return (int)(mDisplayHeight - 2*mVerticalPadding - 2*mTitleMaxLines*title.getLineHeight()); 1562 } 1563 1564 @RequiresApi(26) 1565 static class Api26Impl { Api26Impl()1566 private Api26Impl() { 1567 // This class is not instantiable. 1568 } 1569 setAutofillHints(View view, String... autofillHints)1570 static void setAutofillHints(View view, String... autofillHints) { 1571 view.setAutofillHints(autofillHints); 1572 } 1573 setImportantForAutofill( View view, @SuppressWarnings("SameParameterValue") int mode )1574 static void setImportantForAutofill( 1575 View view, 1576 @SuppressWarnings("SameParameterValue") int mode 1577 ) { 1578 view.setImportantForAutofill(mode); 1579 } 1580 } 1581 } 1582