GuidedActionsStylist.java revision 023bf7d01378f30a63dce3d0a1112eb56bd6b99f
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.animation.AnimatorListenerAdapter; 19import android.animation.AnimatorSet; 20import android.animation.ObjectAnimator; 21import android.content.Context; 22import android.content.pm.PackageManager; 23import android.content.res.Resources; 24import android.content.res.TypedArray; 25import android.graphics.Rect; 26import android.graphics.drawable.Drawable; 27import android.net.Uri; 28import android.os.Build.VERSION; 29import android.support.annotation.NonNull; 30import android.support.v17.leanback.R; 31import android.support.v17.leanback.transition.TransitionHelper; 32import android.support.v17.leanback.transition.TransitionListener; 33import android.support.v17.leanback.widget.VerticalGridView; 34import android.support.v4.content.ContextCompat; 35import android.support.v4.view.ViewCompat; 36import android.support.v7.widget.RecyclerView; 37import android.support.v7.widget.RecyclerView.ViewHolder; 38import android.text.TextUtils; 39import android.util.Log; 40import android.util.TypedValue; 41import android.view.animation.DecelerateInterpolator; 42import android.view.inputmethod.EditorInfo; 43import android.view.Gravity; 44import android.view.LayoutInflater; 45import android.view.View; 46import android.view.ViewGroup; 47import android.view.ViewGroup.LayoutParams; 48import android.view.ViewPropertyAnimator; 49import android.view.ViewTreeObserver; 50import android.view.WindowManager; 51import android.widget.Checkable; 52import android.widget.EditText; 53import android.widget.ImageView; 54import android.widget.TextView; 55 56import java.util.Collections; 57import java.util.List; 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 etc, the other when {@link #setAsButtonActions()} is called is recommended for 65 * 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, or the {@link #onProvideItemLayoutId} method to change the layout 76 * used to display each action. 77 * <p> 78 * Note: If an alternate list layout is provided, the following view IDs must be supplied: 79 * <ul> 80 * <li>{@link android.support.v17.leanback.R.id#guidedactions_list}</li> 81 * </ul><p> 82 * These view IDs must be present in order for the stylist to function. The list ID must correspond 83 * to a {@link VerticalGridView} or subclass. 84 * <p> 85 * If an alternate item layout is provided, the following view IDs should be used to refer to base 86 * elements: 87 * <ul> 88 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_content}</li> 89 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_title}</li> 90 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_description}</li> 91 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_icon}</li> 92 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_checkmark}</li> 93 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_chevron}</li> 94 * </ul><p> 95 * These view IDs are allowed to be missing, in which case the corresponding views in {@link 96 * GuidedActionsStylist.ViewHolder} will be null. 97 * <p> 98 * In order to support editable actions, the view associated with guidedactions_item_title should 99 * be a subclass of {@link android.widget.EditText}, and should satisfy the {@link 100 * ImeKeyMonitor} interface. 101 * 102 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepImeAppearingAnimation 103 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepImeDisappearingAnimation 104 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsSelectorDrawable 105 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsListStyle 106 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedSubActionsListStyle 107 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedButtonActionsListStyle 108 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemContainerStyle 109 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemCheckmarkStyle 110 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemIconStyle 111 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemContentStyle 112 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemTitleStyle 113 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemDescriptionStyle 114 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemChevronStyle 115 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionPressedAnimation 116 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionUnpressedAnimation 117 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionEnabledChevronAlpha 118 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionDisabledChevronAlpha 119 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionTitleMinLines 120 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionTitleMaxLines 121 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionDescriptionMinLines 122 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionVerticalPadding 123 * @see android.R.styleable#Theme_listChoiceIndicatorSingle 124 * @see android.R.styleable#Theme_listChoiceIndicatorMultiple 125 * @see android.support.v17.leanback.app.GuidedStepFragment 126 * @see GuidedAction 127 */ 128public class GuidedActionsStylist implements FragmentAnimationProvider { 129 130 /** 131 * Default viewType that associated with default layout Id for the action item. 132 * @see #getItemViewType(GuidedAction) 133 * @see #onProvideItemLayoutId(int) 134 * @see #onCreateViewHolder(ViewGroup, int) 135 */ 136 public static final int VIEW_TYPE_DEFAULT = 0; 137 138 /** 139 * ViewHolder caches information about the action item layouts' subviews. Subclasses of {@link 140 * GuidedActionsStylist} may also wish to subclass this in order to add fields. 141 * @see GuidedAction 142 */ 143 public static class ViewHolder extends RecyclerView.ViewHolder { 144 145 private GuidedAction mAction; 146 private View mContentView; 147 private TextView mTitleView; 148 private TextView mDescriptionView; 149 private ImageView mIconView; 150 private ImageView mCheckmarkView; 151 private ImageView mChevronView; 152 private boolean mInEditing; 153 private boolean mInEditingDescription; 154 private final boolean mIsSubAction; 155 156 /** 157 * Constructs an ViewHolder and caches the relevant subviews. 158 */ 159 public ViewHolder(View v) { 160 this(v, false); 161 } 162 163 /** 164 * Constructs an ViewHolder for sub action and caches the relevant subviews. 165 */ 166 public ViewHolder(View v, boolean isSubAction) { 167 super(v); 168 169 mContentView = v.findViewById(R.id.guidedactions_item_content); 170 mTitleView = (TextView) v.findViewById(R.id.guidedactions_item_title); 171 mDescriptionView = (TextView) v.findViewById(R.id.guidedactions_item_description); 172 mIconView = (ImageView) v.findViewById(R.id.guidedactions_item_icon); 173 mCheckmarkView = (ImageView) v.findViewById(R.id.guidedactions_item_checkmark); 174 mChevronView = (ImageView) v.findViewById(R.id.guidedactions_item_chevron); 175 mIsSubAction = isSubAction; 176 } 177 178 /** 179 * Returns the content view within this view holder's view, where title and description are 180 * shown. 181 */ 182 public View getContentView() { 183 return mContentView; 184 } 185 186 /** 187 * Returns the title view within this view holder's view. 188 */ 189 public TextView getTitleView() { 190 return mTitleView; 191 } 192 193 /** 194 * Convenience method to return an editable version of the title, if possible, 195 * or null if the title view isn't an EditText. 196 */ 197 public EditText getEditableTitleView() { 198 return (mTitleView instanceof EditText) ? (EditText)mTitleView : null; 199 } 200 201 /** 202 * Returns the description view within this view holder's view. 203 */ 204 public TextView getDescriptionView() { 205 return mDescriptionView; 206 } 207 208 /** 209 * Convenience method to return an editable version of the description, if possible, 210 * or null if the description view isn't an EditText. 211 */ 212 public EditText getEditableDescriptionView() { 213 return (mDescriptionView instanceof EditText) ? (EditText)mDescriptionView : null; 214 } 215 216 /** 217 * Returns the icon view within this view holder's view. 218 */ 219 public ImageView getIconView() { 220 return mIconView; 221 } 222 223 /** 224 * Returns the checkmark view within this view holder's view. 225 */ 226 public ImageView getCheckmarkView() { 227 return mCheckmarkView; 228 } 229 230 /** 231 * Returns the chevron view within this view holder's view. 232 */ 233 public ImageView getChevronView() { 234 return mChevronView; 235 } 236 237 /** 238 * Returns true if the TextView is in editing title or description, false otherwise. 239 */ 240 public boolean isInEditing() { 241 return mInEditing; 242 } 243 244 /** 245 * Returns true if the TextView is in editing description, false otherwise. 246 */ 247 public boolean isInEditingDescription() { 248 return mInEditingDescription; 249 } 250 251 /** 252 * @return Current editing title view or description view or null if not in editing. 253 */ 254 public View getEditingView() { 255 if (mInEditing) { 256 return mInEditingDescription ? mDescriptionView : mTitleView; 257 } else { 258 return null; 259 } 260 } 261 262 /** 263 * @return True if bound action is inside {@link GuidedAction#getSubActions()}, false 264 * otherwise. 265 */ 266 public boolean isSubAction() { 267 return mIsSubAction; 268 } 269 270 /** 271 * @return Currently bound action. 272 */ 273 public GuidedAction getAction() { 274 return mAction; 275 } 276 } 277 278 private static String TAG = "GuidedActionsStylist"; 279 280 private ViewGroup mMainView; 281 private VerticalGridView mActionsGridView; 282 private VerticalGridView mSubActionsGridView; 283 private View mBgView; 284 private View mContentView; 285 private boolean mButtonActions; 286 287 // Cached values from resources 288 private float mEnabledTextAlpha; 289 private float mDisabledTextAlpha; 290 private float mEnabledDescriptionAlpha; 291 private float mDisabledDescriptionAlpha; 292 private float mEnabledChevronAlpha; 293 private float mDisabledChevronAlpha; 294 private int mTitleMinLines; 295 private int mTitleMaxLines; 296 private int mDescriptionMinLines; 297 private int mVerticalPadding; 298 private int mDisplayHeight; 299 300 private GuidedAction mExpandedAction = null; 301 private Object mExpandTransition; 302 303 /** 304 * Creates a view appropriate for displaying a list of GuidedActions, using the provided 305 * inflater and container. 306 * <p> 307 * <i>Note: Does not actually add the created view to the container; the caller should do 308 * this.</i> 309 * @param inflater The layout inflater to be used when constructing the view. 310 * @param container The view group to be passed in the call to 311 * <code>LayoutInflater.inflate</code>. 312 * @return The view to be added to the caller's view hierarchy. 313 */ 314 public View onCreateView(LayoutInflater inflater, ViewGroup container) { 315 mMainView = (ViewGroup) inflater.inflate(onProvideLayoutId(), container, false); 316 mContentView = mMainView.findViewById(mButtonActions ? R.id.guidedactions_content2 : 317 R.id.guidedactions_content); 318 mBgView = mMainView.findViewById(mButtonActions ? R.id.guidedactions_list_background2 : 319 R.id.guidedactions_list_background); 320 if (mMainView instanceof VerticalGridView) { 321 mActionsGridView = (VerticalGridView) mMainView; 322 } else { 323 mActionsGridView = (VerticalGridView) mMainView.findViewById(mButtonActions ? 324 R.id.guidedactions_list2 : R.id.guidedactions_list); 325 if (mActionsGridView == null) { 326 throw new IllegalStateException("No ListView exists."); 327 } 328 mActionsGridView.setWindowAlignmentOffset(0); 329 mActionsGridView.setWindowAlignmentOffsetPercent(50f); 330 mActionsGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); 331 if (!mButtonActions) { 332 mSubActionsGridView = (VerticalGridView) mMainView.findViewById( 333 R.id.guidedactions_sub_list); 334 } 335 } 336 337 // Cache widths, chevron alpha values, max and min text lines, etc 338 Context ctx = mMainView.getContext(); 339 TypedValue val = new TypedValue(); 340 mEnabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionEnabledChevronAlpha); 341 mDisabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionDisabledChevronAlpha); 342 mTitleMinLines = getInteger(ctx, val, R.attr.guidedActionTitleMinLines); 343 mTitleMaxLines = getInteger(ctx, val, R.attr.guidedActionTitleMaxLines); 344 mDescriptionMinLines = getInteger(ctx, val, R.attr.guidedActionDescriptionMinLines); 345 mVerticalPadding = getDimension(ctx, val, R.attr.guidedActionVerticalPadding); 346 mDisplayHeight = ((WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE)) 347 .getDefaultDisplay().getHeight(); 348 349 mEnabledTextAlpha = Float.valueOf(ctx.getResources().getString(R.string 350 .lb_guidedactions_item_unselected_text_alpha)); 351 mDisabledTextAlpha = Float.valueOf(ctx.getResources().getString(R.string 352 .lb_guidedactions_item_disabled_text_alpha)); 353 mEnabledDescriptionAlpha = Float.valueOf(ctx.getResources().getString(R.string 354 .lb_guidedactions_item_unselected_description_text_alpha)); 355 mDisabledDescriptionAlpha = Float.valueOf(ctx.getResources().getString(R.string 356 .lb_guidedactions_item_disabled_description_text_alpha)); 357 return mMainView; 358 } 359 360 /** 361 * Choose the layout resource for button actions in {@link #onProvideLayoutId()}. 362 */ 363 public void setAsButtonActions() { 364 if (mMainView != null) { 365 throw new IllegalStateException("setAsButtonActions() must be called before creating " 366 + "views"); 367 } 368 mButtonActions = true; 369 } 370 371 /** 372 * Returns true if it is button actions list, false for normal actions list. 373 * @return True if it is button actions list, false for normal actions list. 374 */ 375 public boolean isButtonActions() { 376 return mButtonActions; 377 } 378 379 /** 380 * Called when destroy the View created by GuidedActionsStylist. 381 */ 382 public void onDestroyView() { 383 mExpandedAction = null; 384 mExpandTransition = null; 385 mActionsGridView = null; 386 mSubActionsGridView = null; 387 mContentView = null; 388 mBgView = null; 389 mMainView = null; 390 } 391 392 /** 393 * Returns the VerticalGridView that displays the list of GuidedActions. 394 * @return The VerticalGridView for this presenter. 395 */ 396 public VerticalGridView getActionsGridView() { 397 return mActionsGridView; 398 } 399 400 /** 401 * Returns the VerticalGridView that displays the sub actions list of an expanded action. 402 * @return The VerticalGridView that displays the sub actions list of an expanded action. 403 */ 404 public VerticalGridView getSubActionsGridView() { 405 return mSubActionsGridView; 406 } 407 408 /** 409 * Provides the resource ID of the layout defining the host view for the list of guided actions. 410 * Subclasses may override to provide their own customized layouts. The base implementation 411 * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions} or 412 * {@link android.support.v17.leanback.R.layout#lb_guidedbuttonactions} if 413 * {@link #isButtonActions()} is true. If overridden, the substituted layout should contain 414 * matching IDs for any views that should be managed by the base class; this can be achieved by 415 * starting with a copy of the base layout file. 416 * 417 * @return The resource ID of the layout to be inflated to define the host view for the list of 418 * GuidedActions. 419 */ 420 public int onProvideLayoutId() { 421 return mButtonActions ? R.layout.lb_guidedbuttonactions : R.layout.lb_guidedactions; 422 } 423 424 /** 425 * Return view type of action, each different type can have differently associated layout Id. 426 * Default implementation returns {@link #VIEW_TYPE_DEFAULT}. 427 * @param action The action object. 428 * @return View type that used in {@link #onProvideItemLayoutId(int)}. 429 */ 430 public int getItemViewType(GuidedAction action) { 431 return VIEW_TYPE_DEFAULT; 432 } 433 434 /** 435 * Provides the resource ID of the layout defining the view for an individual guided actions. 436 * Subclasses may override to provide their own customized layouts. The base implementation 437 * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions_item}. If overridden, 438 * the substituted layout should contain matching IDs for any views that should be managed by 439 * the base class; this can be achieved by starting with a copy of the base layout file. Note 440 * that in order for the item to support editing, the title view should both subclass {@link 441 * android.widget.EditText} and implement {@link ImeKeyMonitor}; see {@link 442 * GuidedActionEditText}. To support different types of Layouts, override {@link 443 * #onProvideItemLayoutId(int)}. 444 * @return The resource ID of the layout to be inflated to define the view to display an 445 * individual GuidedAction. 446 */ 447 public int onProvideItemLayoutId() { 448 return R.layout.lb_guidedactions_item; 449 } 450 451 /** 452 * Provides the resource ID of the layout defining the view for an individual guided actions. 453 * Subclasses may override to provide their own customized layouts. The base implementation 454 * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions_item}. If overridden, 455 * the substituted layout should contain matching IDs for any views that should be managed by 456 * the base class; this can be achieved by starting with a copy of the base layout file. Note 457 * that in order for the item to support editing, the title view should both subclass {@link 458 * android.widget.EditText} and implement {@link ImeKeyMonitor}; see {@link 459 * GuidedActionEditText}. 460 * @param viewType View type returned by {@link #getItemViewType(GuidedAction)} 461 * @return The resource ID of the layout to be inflated to define the view to display an 462 * individual GuidedAction. 463 */ 464 public int onProvideItemLayoutId(int viewType) { 465 if (viewType == VIEW_TYPE_DEFAULT) { 466 return onProvideItemLayoutId(); 467 } else { 468 throw new RuntimeException("ViewType " + viewType + 469 " not supported in GuidedActionsStylist"); 470 } 471 } 472 473 /** 474 * Constructs a {@link ViewHolder} capable of representing {@link GuidedAction}s. Subclasses 475 * may choose to return a subclass of ViewHolder. To support different view types, override 476 * {@link #onCreateViewHolder(ViewGroup, int)} 477 * <p> 478 * <i>Note: Should not actually add the created view to the parent; the caller will do 479 * this.</i> 480 * @param parent The view group to be used as the parent of the new view. 481 * @return The view to be added to the caller's view hierarchy. 482 */ 483 public ViewHolder onCreateViewHolder(ViewGroup parent) { 484 LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 485 View v = inflater.inflate(onProvideItemLayoutId(), parent, false); 486 return new ViewHolder(v, parent == mSubActionsGridView); 487 } 488 489 /** 490 * Constructs a {@link ViewHolder} capable of representing {@link GuidedAction}s. Subclasses 491 * may choose to return a subclass of ViewHolder. 492 * <p> 493 * <i>Note: Should not actually add the created view to the parent; the caller will do 494 * this.</i> 495 * @param parent The view group to be used as the parent of the new view. 496 * @param viewType The viewType returned by {@link #getItemViewType(GuidedAction)} 497 * @return The view to be added to the caller's view hierarchy. 498 */ 499 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 500 if (viewType == VIEW_TYPE_DEFAULT) { 501 return onCreateViewHolder(parent); 502 } 503 LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 504 View v = inflater.inflate(onProvideItemLayoutId(viewType), parent, false); 505 return new ViewHolder(v, parent == mSubActionsGridView); 506 } 507 508 /** 509 * Binds a {@link ViewHolder} to a particular {@link GuidedAction}. 510 * @param vh The view holder to be associated with the given action. 511 * @param action The guided action to be displayed by the view holder's view. 512 * @return The view to be added to the caller's view hierarchy. 513 */ 514 public void onBindViewHolder(ViewHolder vh, GuidedAction action) { 515 516 vh.mAction = action; 517 if (vh.mTitleView != null) { 518 vh.mTitleView.setText(action.getTitle()); 519 vh.mTitleView.setAlpha(action.isEnabled() ? mEnabledTextAlpha : mDisabledTextAlpha); 520 vh.mTitleView.setFocusable(action.isEditable()); 521 } 522 if (vh.mDescriptionView != null) { 523 vh.mDescriptionView.setText(action.getDescription()); 524 vh.mDescriptionView.setVisibility(TextUtils.isEmpty(action.getDescription()) ? 525 View.GONE : View.VISIBLE); 526 vh.mDescriptionView.setAlpha(action.isEnabled() ? mEnabledDescriptionAlpha : 527 mDisabledDescriptionAlpha); 528 vh.mDescriptionView.setFocusable(action.isDescriptionEditable()); 529 } 530 // Clients might want the check mark view to be gone entirely, in which case, ignore it. 531 if (vh.mCheckmarkView != null) { 532 onBindCheckMarkView(vh, action); 533 } 534 setIcon(vh.mIconView, action); 535 536 if (action.hasMultilineDescription()) { 537 if (vh.mTitleView != null) { 538 vh.mTitleView.setMaxLines(mTitleMaxLines); 539 if (vh.mDescriptionView != null) { 540 vh.mDescriptionView.setMaxHeight(getDescriptionMaxHeight( 541 vh.itemView.getContext(), vh.mTitleView)); 542 } 543 } 544 } else { 545 if (vh.mTitleView != null) { 546 vh.mTitleView.setMaxLines(mTitleMinLines); 547 } 548 if (vh.mDescriptionView != null) { 549 vh.mDescriptionView.setMaxLines(mDescriptionMinLines); 550 } 551 } 552 setEditingMode(vh, action, false); 553 if (action.isFocusable()) { 554 vh.itemView.setFocusable(true); 555 ((ViewGroup) vh.itemView).setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); 556 } else { 557 vh.itemView.setFocusable(false); 558 ((ViewGroup) vh.itemView).setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); 559 } 560 setupImeOptions(vh, action); 561 562 updateChevronAndVisibility(vh); 563 } 564 565 /** 566 * Called by {@link #onBindViewHolder(ViewHolder, GuidedAction)} to setup IME options. Default 567 * implementation assigns {@link EditorInfo#IME_ACTION_DONE}. Subclass may override. 568 * @param vh The view holder to be associated with the given action. 569 * @param action The guided action to be displayed by the view holder's view. 570 */ 571 protected void setupImeOptions(ViewHolder vh, GuidedAction action) { 572 setupNextImeOptions(vh.getEditableTitleView()); 573 setupNextImeOptions(vh.getEditableDescriptionView()); 574 } 575 576 private void setupNextImeOptions(EditText edit) { 577 if (edit != null) { 578 edit.setImeOptions(EditorInfo.IME_ACTION_NEXT); 579 } 580 } 581 582 public void setEditingMode(ViewHolder vh, GuidedAction action, boolean editing) { 583 if (editing != vh.mInEditing) { 584 vh.mInEditing = editing; 585 onEditingModeChange(vh, action, editing); 586 } 587 } 588 589 protected void onEditingModeChange(ViewHolder vh, GuidedAction action, boolean editing) { 590 action = vh.getAction(); 591 TextView titleView = vh.getTitleView(); 592 TextView descriptionView = vh.getDescriptionView(); 593 if (editing) { 594 CharSequence editTitle = action.getEditTitle(); 595 if (titleView != null && editTitle != null) { 596 titleView.setText(editTitle); 597 } 598 CharSequence editDescription = action.getEditDescription(); 599 if (descriptionView != null && editDescription != null) { 600 descriptionView.setText(editDescription); 601 } 602 if (action.isDescriptionEditable()) { 603 if (descriptionView != null) { 604 descriptionView.setVisibility(View.VISIBLE); 605 descriptionView.setInputType(action.getDescriptionEditInputType()); 606 } 607 vh.mInEditingDescription = true; 608 } else { 609 vh.mInEditingDescription = false; 610 if (titleView != null) { 611 titleView.setInputType(action.getEditInputType()); 612 } 613 } 614 } else { 615 if (titleView != null) { 616 titleView.setText(action.getTitle()); 617 } 618 if (descriptionView != null) { 619 descriptionView.setText(action.getDescription()); 620 } 621 if (vh.mInEditingDescription) { 622 if (descriptionView != null) { 623 descriptionView.setVisibility(TextUtils.isEmpty(action.getDescription()) ? 624 View.GONE : View.VISIBLE); 625 descriptionView.setInputType(action.getDescriptionInputType()); 626 } 627 vh.mInEditingDescription = false; 628 } else { 629 if (titleView != null) { 630 titleView.setInputType(action.getInputType()); 631 } 632 } 633 } 634 } 635 636 /** 637 * Animates the view holder's view (or subviews thereof) when the action has had its focus 638 * state changed. 639 * @param vh The view holder associated with the relevant action. 640 * @param focused True if the action has become focused, false if it has lost focus. 641 */ 642 public void onAnimateItemFocused(ViewHolder vh, boolean focused) { 643 // No animations for this, currently, because the animation is done on 644 // mSelectorView 645 } 646 647 /** 648 * Animates the view holder's view (or subviews thereof) when the action has had its press 649 * state changed. 650 * @param vh The view holder associated with the relevant action. 651 * @param pressed True if the action has been pressed, false if it has been unpressed. 652 */ 653 public void onAnimateItemPressed(ViewHolder vh, boolean pressed) { 654 int attr = pressed ? R.attr.guidedActionPressedAnimation : 655 R.attr.guidedActionUnpressedAnimation; 656 createAnimator(vh.itemView, attr).start(); 657 } 658 659 /** 660 * Resets the view holder's view to unpressed state. 661 * @param vh The view holder associated with the relevant action. 662 */ 663 public void onAnimateItemPressedCancelled(ViewHolder vh) { 664 createAnimator(vh.itemView, R.attr.guidedActionUnpressedAnimation).end(); 665 } 666 667 /** 668 * Animates the view holder's view (or subviews thereof) when the action has had its check state 669 * changed. Default implementation calls setChecked() if {@link ViewHolder#getCheckmarkView()} 670 * is instance of {@link Checkable}. 671 * 672 * @param vh The view holder associated with the relevant action. 673 * @param checked True if the action has become checked, false if it has become unchecked. 674 * @see #onBindCheckMarkView(ViewHolder, GuidedAction) 675 */ 676 public void onAnimateItemChecked(ViewHolder vh, boolean checked) { 677 if (vh.mCheckmarkView instanceof Checkable) { 678 ((Checkable) vh.mCheckmarkView).setChecked(checked); 679 } 680 } 681 682 /** 683 * Sets states of check mark view, called by {@link #onBindViewHolder(ViewHolder, GuidedAction)} 684 * when action's checkset Id is other than {@link GuidedAction#NO_CHECK_SET}. Default 685 * implementation assigns drawable loaded from theme attribute 686 * {@link android.R.attr#listChoiceIndicatorMultiple} for checkbox or 687 * {@link android.R.attr#listChoiceIndicatorSingle} for radio button. Subclass rarely needs 688 * override the method, instead app can provide its own drawable that supports transition 689 * animations, change theme attributes {@link android.R.attr#listChoiceIndicatorMultiple} and 690 * {@link android.R.attr#listChoiceIndicatorSingle} in {android.support.v17.leanback.R. 691 * styleable#LeanbackGuidedStepTheme}. 692 * 693 * @param vh The view holder associated with the relevant action. 694 * @param action The GuidedAction object to bind to. 695 * @see #onAnimateItemChecked(ViewHolder, boolean) 696 */ 697 public void onBindCheckMarkView(ViewHolder vh, GuidedAction action) { 698 if (action.getCheckSetId() != GuidedAction.NO_CHECK_SET) { 699 vh.mCheckmarkView.setVisibility(View.VISIBLE); 700 int attrId = action.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID ? 701 android.R.attr.listChoiceIndicatorMultiple : 702 android.R.attr.listChoiceIndicatorSingle; 703 final Context context = vh.mCheckmarkView.getContext(); 704 Drawable drawable = null; 705 TypedValue typedValue = new TypedValue(); 706 if (context.getTheme().resolveAttribute(attrId, typedValue, true)) { 707 drawable = ContextCompat.getDrawable(context, typedValue.resourceId); 708 } 709 vh.mCheckmarkView.setImageDrawable(drawable); 710 if (vh.mCheckmarkView instanceof Checkable) { 711 ((Checkable) vh.mCheckmarkView).setChecked(action.isChecked()); 712 } 713 } else { 714 vh.mCheckmarkView.setVisibility(View.GONE); 715 } 716 } 717 718 /** 719 * Sets states of chevron view, called by {@link #onBindViewHolder(ViewHolder, GuidedAction)}. 720 * Subclass may override. 721 * 722 * @param vh The view holder associated with the relevant action. 723 * @param action The GuidedAction object to bind to. 724 */ 725 public void onBindChevronView(ViewHolder vh, GuidedAction action) { 726 final boolean hasNext = action.hasNext(); 727 final boolean hasSubActions = action.hasSubActions(); 728 if (hasNext || hasSubActions) { 729 vh.mChevronView.setVisibility(View.VISIBLE); 730 vh.mChevronView.setAlpha(action.isEnabled() ? mEnabledChevronAlpha : 731 mDisabledChevronAlpha); 732 if (hasNext) { 733 float r = mMainView != null 734 && mMainView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? 180f : 0f; 735 vh.mChevronView.setRotation(r); 736 } else if (action == mExpandedAction) { 737 vh.mChevronView.setRotation(270); 738 } else { 739 vh.mChevronView.setRotation(90); 740 } 741 } else { 742 vh.mChevronView.setVisibility(View.GONE); 743 744 } 745 } 746 747 /** 748 * Expands or collapse the sub actions list view. 749 * @param avh When not null, fill sub actions list of this ViewHolder into sub actions list and 750 * hide the other items in main list. When null, collapse the sub actions list. 751 */ 752 public void setExpandedViewHolder(ViewHolder avh) { 753 if (mSubActionsGridView == null || isInExpandTransition()) { 754 return; 755 } 756 if (isExpandTransitionSupported()) { 757 startExpandedTransition(avh); 758 } else { 759 onUpdateExpandedViewHolder(avh); 760 } 761 } 762 763 /** 764 * Returns true if it is running an expanding or collapsing transition, false otherwise. 765 * @return True if it is running an expanding or collapsing transition, false otherwise. 766 */ 767 public boolean isInExpandTransition() { 768 return mExpandTransition != null; 769 } 770 771 /** 772 * Returns if expand/collapse animation is supported. When this method returns true, 773 * {@link #startExpandedTransition(ViewHolder)} will be used. When this method returns false, 774 * {@link #onUpdateExpandedViewHolder(ViewHolder)} will be called. 775 * @return True if it is running an expanding or collapsing transition, false otherwise. 776 */ 777 public boolean isExpandTransitionSupported() { 778 return VERSION.SDK_INT >= 21; 779 } 780 781 /** 782 * Start transition to expand or collapse GuidedActionStylist. 783 * @param avh When not null, the GuidedActionStylist expands the sub actions of avh. When null 784 * the GuidedActionStylist will collapse sub actions. 785 */ 786 public void startExpandedTransition(ViewHolder avh) { 787 ViewHolder focusAvh = null; // expand / collapse view holder 788 final int count = mActionsGridView.getChildCount(); 789 for (int i = 0; i < count; i++) { 790 ViewHolder vh = (ViewHolder) mActionsGridView 791 .getChildViewHolder(mActionsGridView.getChildAt(i)); 792 if (avh == null && vh.itemView.getVisibility() == View.VISIBLE) { 793 // going to collapse this one. 794 focusAvh = vh; 795 break; 796 } else if (avh != null && vh.getAction() == avh.getAction()) { 797 // going to expand this one. 798 focusAvh = vh; 799 break; 800 } 801 } 802 if (focusAvh == null) { 803 // huh? 804 onUpdateExpandedViewHolder(avh); 805 return; 806 } 807 Object set = TransitionHelper.createTransitionSet(false); 808 Object slideAndFade = TransitionHelper.createFadeAndShortSlide(Gravity.TOP | Gravity.BOTTOM, 809 (float) focusAvh.itemView.getHeight()); 810 Object changeFocusItemTransform = TransitionHelper.createChangeTransform(); 811 Object changeFocusItemBounds = TransitionHelper.createChangeBounds(false); 812 Object fadeGrid = TransitionHelper.createFadeTransition(TransitionHelper.FADE_IN | 813 TransitionHelper.FADE_OUT); 814 Object changeGridBounds = TransitionHelper.createChangeBounds(false); 815 if (avh == null) { 816 TransitionHelper.setStartDelay(slideAndFade, 150); 817 TransitionHelper.setStartDelay(changeFocusItemTransform, 100); 818 TransitionHelper.setStartDelay(changeFocusItemBounds, 100); 819 } else { 820 TransitionHelper.setStartDelay(fadeGrid, 100); 821 TransitionHelper.setStartDelay(changeGridBounds, 100); 822 TransitionHelper.setStartDelay(changeFocusItemTransform, 50); 823 TransitionHelper.setStartDelay(changeFocusItemBounds, 50); 824 } 825 for (int i = 0; i < count; i++) { 826 ViewHolder vh = (ViewHolder) mActionsGridView 827 .getChildViewHolder(mActionsGridView.getChildAt(i)); 828 if (vh == focusAvh) { 829 // going to expand/collapse this one. 830 TransitionHelper.include(changeFocusItemTransform, vh.itemView); 831 TransitionHelper.include(changeFocusItemBounds, vh.itemView); 832 } else { 833 // going to slide this item to top / bottom. 834 TransitionHelper.include(slideAndFade, vh.itemView); 835 } 836 } 837 TransitionHelper.include(fadeGrid, mSubActionsGridView); 838 TransitionHelper.include(changeGridBounds, mSubActionsGridView); 839 TransitionHelper.addTransition(set, slideAndFade); 840 TransitionHelper.addTransition(set, changeFocusItemTransform); 841 TransitionHelper.addTransition(set, changeFocusItemBounds); 842 TransitionHelper.addTransition(set, fadeGrid); 843 TransitionHelper.addTransition(set, changeGridBounds); 844 mExpandTransition = set; 845 TransitionHelper.addTransitionListener(mExpandTransition, new TransitionListener() { 846 @Override 847 public void onTransitionEnd(Object transition) { 848 mExpandTransition = null; 849 } 850 }); 851 if (avh != null && mSubActionsGridView.getTop() != avh.itemView.getTop()) { 852 // For expanding, set the initial position of subActionsGridView before running 853 // a ChangeBounds on it. 854 final ViewHolder toUpdate = avh; 855 mSubActionsGridView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 856 @Override 857 public void onLayoutChange(View v, int left, int top, int right, int bottom, 858 int oldLeft, int oldTop, int oldRight, int oldBottom) { 859 if (mSubActionsGridView == null) { 860 return; 861 } 862 mSubActionsGridView.removeOnLayoutChangeListener(this); 863 mMainView.post(new Runnable() { 864 public void run() { 865 if (mMainView == null) { 866 return; 867 } 868 TransitionHelper.beginDelayedTransition(mMainView, mExpandTransition); 869 onUpdateExpandedViewHolder(toUpdate); 870 } 871 }); 872 } 873 }); 874 ViewGroup.MarginLayoutParams lp = 875 (ViewGroup.MarginLayoutParams) mSubActionsGridView.getLayoutParams(); 876 lp.topMargin = avh.itemView.getTop(); 877 lp.height = 0; 878 mSubActionsGridView.setLayoutParams(lp); 879 return; 880 } 881 TransitionHelper.beginDelayedTransition(mMainView, mExpandTransition); 882 onUpdateExpandedViewHolder(avh); 883 } 884 885 /** 886 * @return True if sub actions list is expanded. 887 */ 888 public boolean isSubActionsExpanded() { 889 return mExpandedAction != null; 890 } 891 892 /** 893 * @return Current expanded GuidedAction or null if not expanded. 894 */ 895 public GuidedAction getExpandedAction() { 896 return mExpandedAction; 897 } 898 899 /** 900 * Expand or collapse GuidedActionStylist. 901 * @param avh When not null, the GuidedActionStylist expands the sub actions of avh. When null 902 * the GuidedActionStylist will collapse sub actions. 903 */ 904 public void onUpdateExpandedViewHolder(ViewHolder avh) { 905 if (avh == null) { 906 mExpandedAction = null; 907 } else if (avh.getAction() != mExpandedAction) { 908 mExpandedAction = avh.getAction(); 909 } 910 // In expanding mode, notifyItemChange on expanded item will reset the translationY by 911 // the default ItemAnimator. So disable ItemAnimation in expanding mode. 912 mActionsGridView.setAnimateChildLayout(false); 913 final int count = mActionsGridView.getChildCount(); 914 for (int i = 0; i < count; i++) { 915 ViewHolder vh = (ViewHolder) mActionsGridView 916 .getChildViewHolder(mActionsGridView.getChildAt(i)); 917 updateChevronAndVisibility(vh); 918 } 919 if (mSubActionsGridView != null) { 920 if (avh != null) { 921 ViewGroup.MarginLayoutParams lp = 922 (ViewGroup.MarginLayoutParams) mSubActionsGridView.getLayoutParams(); 923 lp.topMargin = avh.itemView.getTop(); 924 lp.height = ViewGroup.MarginLayoutParams.MATCH_PARENT; 925 mSubActionsGridView.setLayoutParams(lp); 926 mSubActionsGridView.setVisibility(View.VISIBLE); 927 mSubActionsGridView.requestFocus(); 928 mSubActionsGridView.setSelectedPosition(0); 929 ((GuidedActionAdapter) mSubActionsGridView.getAdapter()) 930 .setActions(avh.getAction().getSubActions()); 931 } else { 932 mSubActionsGridView.setVisibility(View.INVISIBLE); 933 ViewGroup.MarginLayoutParams lp = 934 (ViewGroup.MarginLayoutParams) mSubActionsGridView.getLayoutParams(); 935 lp.height = 0; 936 mSubActionsGridView.setLayoutParams(lp); 937 ((GuidedActionAdapter) mSubActionsGridView.getAdapter()) 938 .setActions(Collections.EMPTY_LIST); 939 mActionsGridView.requestFocus(); 940 } 941 } 942 } 943 944 private void updateChevronAndVisibility(ViewHolder vh) { 945 if (!vh.isSubAction()) { 946 if (mExpandedAction == null) { 947 vh.itemView.setVisibility(View.VISIBLE); 948 vh.itemView.setTranslationY(0); 949 } else if (vh.getAction() == mExpandedAction) { 950 vh.itemView.setVisibility(View.VISIBLE); 951 vh.itemView.setTranslationY(- vh.itemView.getHeight()); 952 } else { 953 vh.itemView.setVisibility(View.INVISIBLE); 954 vh.itemView.setTranslationY(0); 955 } 956 } 957 if (vh.mChevronView != null) { 958 onBindChevronView(vh, vh.getAction()); 959 } 960 } 961 962 /* 963 * ========================================== 964 * FragmentAnimationProvider overrides 965 * ========================================== 966 */ 967 968 /** 969 * {@inheritDoc} 970 */ 971 @Override 972 public void onImeAppearing(@NonNull List<Animator> animators) { 973 animators.add(createAnimator(mContentView, R.attr.guidedStepImeAppearingAnimation)); 974 } 975 976 /** 977 * {@inheritDoc} 978 */ 979 @Override 980 public void onImeDisappearing(@NonNull List<Animator> animators) { 981 animators.add(createAnimator(mContentView, R.attr.guidedStepImeDisappearingAnimation)); 982 } 983 984 /* 985 * ========================================== 986 * Private methods 987 * ========================================== 988 */ 989 990 private float getFloat(Context ctx, TypedValue typedValue, int attrId) { 991 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 992 // Android resources don't have a native float type, so we have to use strings. 993 return Float.valueOf(ctx.getResources().getString(typedValue.resourceId)); 994 } 995 996 private int getInteger(Context ctx, TypedValue typedValue, int attrId) { 997 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 998 return ctx.getResources().getInteger(typedValue.resourceId); 999 } 1000 1001 private int getDimension(Context ctx, TypedValue typedValue, int attrId) { 1002 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 1003 return ctx.getResources().getDimensionPixelSize(typedValue.resourceId); 1004 } 1005 1006 private static Animator createAnimator(View v, int attrId) { 1007 Context ctx = v.getContext(); 1008 TypedValue typedValue = new TypedValue(); 1009 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 1010 Animator animator = AnimatorInflater.loadAnimator(ctx, typedValue.resourceId); 1011 animator.setTarget(v); 1012 return animator; 1013 } 1014 1015 private boolean setIcon(final ImageView iconView, GuidedAction action) { 1016 Drawable icon = null; 1017 if (iconView != null) { 1018 Context context = iconView.getContext(); 1019 icon = action.getIcon(); 1020 if (icon != null) { 1021 // setImageDrawable resets the drawable's level unless we set the view level first. 1022 iconView.setImageLevel(icon.getLevel()); 1023 iconView.setImageDrawable(icon); 1024 iconView.setVisibility(View.VISIBLE); 1025 } else { 1026 iconView.setVisibility(View.GONE); 1027 } 1028 } 1029 return icon != null; 1030 } 1031 1032 /** 1033 * @return the max height in pixels the description can be such that the 1034 * action nicely takes up the entire screen. 1035 */ 1036 private int getDescriptionMaxHeight(Context context, TextView title) { 1037 // The 2 multiplier on the title height calculation is a 1038 // conservative estimate for font padding which can not be 1039 // calculated at this stage since the view hasn't been rendered yet. 1040 return (int)(mDisplayHeight - 2*mVerticalPadding - 2*mTitleMaxLines*title.getLineHeight()); 1041 } 1042 1043} 1044