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