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