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