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