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