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