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