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