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