GuidedActionsStylist.java revision ebd3d9078dbaebd10a9506ca086435eb63e8a2d2
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.v7.widget.RecyclerView; 31import android.support.v7.widget.RecyclerView.ViewHolder; 32import android.text.TextUtils; 33import android.util.Log; 34import android.util.TypedValue; 35import android.view.animation.DecelerateInterpolator; 36import android.view.LayoutInflater; 37import android.view.View; 38import android.view.ViewGroup; 39import android.view.ViewGroup.LayoutParams; 40import android.view.ViewPropertyAnimator; 41import android.view.ViewTreeObserver; 42import android.view.WindowManager; 43import android.widget.ImageView; 44import android.widget.TextView; 45 46import java.util.List; 47 48/** 49 * GuidedActionsStylist is used within a GuidedStepFragment to supply the right-side panel 50 * where users can take actions. It consists of a container for the list of actions, and a 51 * stationary selector view that indicates visually the location of focus. 52 * <p> 53 * Many aspects of the base GuidedActionsStylist can be customized through theming; see the 54 * theme attributes below. Note that these attributes are not set on individual elements in layout 55 * XML, but instead would be set in a custom theme. See 56 * <a href="http://developer.android.com/guide/topics/ui/themes.html">Styles and Themes</a> 57 * for more information. 58 * <p> 59 * If these hooks are insufficient, this class may also be subclassed. Subclasses may wish to 60 * override the {@link #onProvideLayoutId} method to change the layout used to display the 61 * list container and selector, or the {@link #onProvideItemLayoutId} method to change the layout 62 * used to display each action. 63 * <p> 64 * Note: If an alternate list layout is provided, the following view IDs must be supplied: 65 * <ul> 66 * <li>{@link android.support.v17.leanback.R.id#guidedactions_selector}</li> 67 * <li>{@link android.support.v17.leanback.R.id#guidedactions_list}</li> 68 * </ul><p> 69 * These view IDs must be present in order for the stylist to function. The list ID must correspond 70 * to a {@link VerticalGridView} or subclass. 71 * <p> 72 * If an alternate item layout is provided, the following view IDs should be used to refer to base 73 * elements: 74 * <ul> 75 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_content}</li> 76 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_title}</li> 77 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_description}</li> 78 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_icon}</li> 79 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_checkmark}</li> 80 * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_chevron}</li> 81 * </ul><p> 82 * These view IDs are allowed to be missing, in which case the corresponding views in {@link 83 * GuidedActionsStylist.ViewHolder} will be null. 84 * 85 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsEntryAnimation 86 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsSelectorShowAnimation 87 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsSelectorHideAnimation 88 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsContainerStyle 89 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsSelectorStyle 90 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsListStyle 91 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemContainerStyle 92 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemCheckmarkStyle 93 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemIconStyle 94 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemContentStyle 95 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemTitleStyle 96 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemDescriptionStyle 97 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemChevronStyle 98 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionCheckedAnimation 99 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionUncheckedAnimation 100 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionPressedAnimation 101 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionUnpressedAnimation 102 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionEnabledChevronAlpha 103 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionDisabledChevronAlpha 104 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionContentWidth 105 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionContentWidthNoIcon 106 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionTitleMinLines 107 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionTitleMaxLines 108 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionDescriptionMinLines 109 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionVerticalPadding 110 * @see android.support.v17.leanback.app.GuidedStepFragment 111 * @see GuidedAction 112 */ 113public class GuidedActionsStylist implements FragmentAnimationProvider { 114 115 /** 116 * ViewHolder caches information about the action item layouts' subviews. Subclasses of {@link 117 * GuidedActionsStylist} may also wish to subclass this in order to add fields. 118 * @see GuidedAction 119 */ 120 public static class ViewHolder { 121 122 public final View view; 123 124 private View mContentView; 125 private TextView mTitleView; 126 private TextView mDescriptionView; 127 private ImageView mIconView; 128 private ImageView mCheckmarkView; 129 private ImageView mChevronView; 130 131 /** 132 * Constructs an ViewHolder and caches the relevant subviews. 133 */ 134 public ViewHolder(View v) { 135 view = v; 136 137 mContentView = v.findViewById(R.id.guidedactions_item_content); 138 mTitleView = (TextView) v.findViewById(R.id.guidedactions_item_title); 139 mDescriptionView = (TextView) v.findViewById(R.id.guidedactions_item_description); 140 mIconView = (ImageView) v.findViewById(R.id.guidedactions_item_icon); 141 mCheckmarkView = (ImageView) v.findViewById(R.id.guidedactions_item_checkmark); 142 mChevronView = (ImageView) v.findViewById(R.id.guidedactions_item_chevron); 143 } 144 145 /** 146 * Returns the content view within this view holder's view, where title and description are 147 * shown. 148 */ 149 public View getContentView() { 150 return mContentView; 151 } 152 153 /** 154 * Returns the title view within this view holder's view. 155 */ 156 public TextView getTitleView() { 157 return mTitleView; 158 } 159 160 /** 161 * Returns the description view within this view holder's view. 162 */ 163 public TextView getDescriptionView() { 164 return mDescriptionView; 165 } 166 167 /** 168 * Returns the icon view within this view holder's view. 169 */ 170 public ImageView getIconView() { 171 return mIconView; 172 } 173 174 /** 175 * Returns the checkmark view within this view holder's view. 176 */ 177 public ImageView getCheckmarkView() { 178 return mCheckmarkView; 179 } 180 181 /** 182 * Returns the chevron view within this view holder's view. 183 */ 184 public ImageView getChevronView() { 185 return mChevronView; 186 } 187 188 } 189 190 private static String TAG = "GuidedActionsStylist"; 191 192 protected View mMainView; 193 protected VerticalGridView mActionsGridView; 194 protected View mSelectorView; 195 196 // Cached values from resources 197 private float mEnabledChevronAlpha; 198 private float mDisabledChevronAlpha; 199 private int mContentWidth; 200 private int mContentWidthNoIcon; 201 private int mTitleMinLines; 202 private int mTitleMaxLines; 203 private int mDescriptionMinLines; 204 private int mVerticalPadding; 205 private int mDisplayHeight; 206 207 /** 208 * Creates a view appropriate for displaying a list of GuidedActions, using the provided 209 * inflater and container. 210 * <p> 211 * <i>Note: Does not actually add the created view to the container; the caller should do 212 * this.</i> 213 * @param inflater The layout inflater to be used when constructing the view. 214 * @param container The view group to be passed in the call to 215 * <code>LayoutInflater.inflate</code>. 216 * @return The view to be added to the caller's view hierarchy. 217 */ 218 public View onCreateView(LayoutInflater inflater, ViewGroup container) { 219 mMainView = inflater.inflate(onProvideLayoutId(), container, false); 220 mSelectorView = mMainView.findViewById(R.id.guidedactions_selector); 221 if (mMainView instanceof VerticalGridView) { 222 mActionsGridView = (VerticalGridView) mMainView; 223 } else { 224 mActionsGridView = (VerticalGridView) mMainView.findViewById(R.id.guidedactions_list); 225 if (mActionsGridView == null) { 226 throw new IllegalStateException("No ListView exists."); 227 } 228 mActionsGridView.setWindowAlignmentOffset(0); 229 mActionsGridView.setWindowAlignmentOffsetPercent(50f); 230 mActionsGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); 231 if (mSelectorView != null) { 232 mActionsGridView.setOnScrollListener(new 233 SelectorAnimator(mSelectorView, mActionsGridView)); 234 } 235 } 236 237 mActionsGridView.requestFocusFromTouch(); 238 239 if (mSelectorView != null) { 240 // ALlow focus to move to other views 241 mActionsGridView.getViewTreeObserver().addOnGlobalFocusChangeListener( 242 new ViewTreeObserver.OnGlobalFocusChangeListener() { 243 private boolean mChildFocused; 244 245 @Override 246 public void onGlobalFocusChanged(View oldFocus, View newFocus) { 247 View focusedChild = mActionsGridView.getFocusedChild(); 248 if (focusedChild == null) { 249 mSelectorView.setVisibility(View.INVISIBLE); 250 mChildFocused = false; 251 } else if (!mChildFocused) { 252 mChildFocused = true; 253 mSelectorView.setVisibility(View.VISIBLE); 254 updateSelectorView(focusedChild); 255 } 256 } 257 }); 258 } 259 260 // Cache widths, chevron alpha values, max and min text lines, etc 261 Context ctx = mMainView.getContext(); 262 TypedValue val = new TypedValue(); 263 mEnabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionEnabledChevronAlpha); 264 mDisabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionDisabledChevronAlpha); 265 mContentWidth = getDimension(ctx, val, R.attr.guidedActionContentWidth); 266 mContentWidthNoIcon = getDimension(ctx, val, R.attr.guidedActionContentWidthNoIcon); 267 mTitleMinLines = getInteger(ctx, val, R.attr.guidedActionTitleMinLines); 268 mTitleMaxLines = getInteger(ctx, val, R.attr.guidedActionTitleMaxLines); 269 mDescriptionMinLines = getInteger(ctx, val, R.attr.guidedActionDescriptionMinLines); 270 mVerticalPadding = getDimension(ctx, val, R.attr.guidedActionVerticalPadding); 271 mDisplayHeight = ((WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE)) 272 .getDefaultDisplay().getHeight(); 273 274 return mMainView; 275 } 276 277 /** 278 * Returns the VerticalGridView that displays the list of GuidedActions. 279 * @return The VerticalGridView for this presenter. 280 */ 281 public VerticalGridView getActionsGridView() { 282 return mActionsGridView; 283 } 284 285 /** 286 * Provides the resource ID of the layout defining the host view for the list of guided actions. 287 * Subclasses may override to provide their own customized layouts. The base implementation 288 * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions}. If overridden, the 289 * substituted layout should contain matching IDs for any views that should be managed by the 290 * base class; this can be achieved by starting with a copy of the base layout file. 291 * @return The resource ID of the layout to be inflated to define the host view for the list 292 * of GuidedActions. 293 */ 294 public int onProvideLayoutId() { 295 return R.layout.lb_guidedactions; 296 } 297 298 /** 299 * Provides the resource ID of the layout defining the view for an individual guided actions. 300 * Subclasses may override to provide their own customized layouts. The base implementation 301 * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions_item}. If overridden, 302 * the substituted layout should contain matching IDs for any views that should be managed by 303 * the base class; this can be achieved by starting with a copy of the base layout file. 304 * @return The resource ID of the layout to be inflated to define the view to display an 305 * individual GuidedAction. 306 */ 307 public int onProvideItemLayoutId() { 308 return R.layout.lb_guidedactions_item; 309 } 310 311 /** 312 * Constructs a {@link ViewHolder} capable of representing {@link GuidedAction}s. Subclasses 313 * may choose to return a subclass of ViewHolder. 314 * <p> 315 * <i>Note: Should not actually add the created view to the parent; the caller will do 316 * this.</i> 317 * @param parent The view group to be used as the parent of the new view. 318 * @return The view to be added to the caller's view hierarchy. 319 */ 320 public ViewHolder onCreateViewHolder(ViewGroup parent) { 321 LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 322 View v = inflater.inflate(onProvideItemLayoutId(), parent, false); 323 return new ViewHolder(v); 324 } 325 326 /** 327 * Binds a {@link ViewHolder} to a particular {@link GuidedAction}. 328 * @param vh The view holder to be associated with the given action. 329 * @param action The guided action to be displayed by the view holder's view. 330 * @return The view to be added to the caller's view hierarchy. 331 */ 332 public void onBindViewHolder(ViewHolder vh, GuidedAction action) { 333 334 if (vh.mTitleView != null) { 335 vh.mTitleView.setText(action.getTitle()); 336 } 337 if (vh.mDescriptionView != null) { 338 vh.mDescriptionView.setText(action.getDescription()); 339 vh.mDescriptionView.setVisibility(TextUtils.isEmpty(action.getDescription()) ? 340 View.GONE : View.VISIBLE); 341 } 342 // Clients might want the check mark view to be gone entirely, in which case, ignore it. 343 if (vh.mCheckmarkView != null && vh.mCheckmarkView.getVisibility() != View.GONE) { 344 vh.mCheckmarkView.setVisibility(action.isChecked() ? View.VISIBLE : View.INVISIBLE); 345 } 346 347 if (vh.mContentView != null) { 348 ViewGroup.LayoutParams contentLp = vh.mContentView.getLayoutParams(); 349 if (setIcon(vh.mIconView, action)) { 350 contentLp.width = mContentWidth; 351 } else { 352 contentLp.width = mContentWidthNoIcon; 353 } 354 vh.mContentView.setLayoutParams(contentLp); 355 } 356 357 if (vh.mChevronView != null) { 358 vh.mChevronView.setVisibility(action.hasNext() ? View.VISIBLE : View.INVISIBLE); 359 vh.mChevronView.setAlpha(action.isEnabled() ? mEnabledChevronAlpha : 360 mDisabledChevronAlpha); 361 } 362 363 if (action.hasMultilineDescription()) { 364 if (vh.mTitleView != null) { 365 vh.mTitleView.setMaxLines(mTitleMaxLines); 366 if (vh.mDescriptionView != null) { 367 vh.mDescriptionView.setMaxHeight(getDescriptionMaxHeight(vh.view.getContext(), 368 vh.mTitleView)); 369 } 370 } 371 } else { 372 if (vh.mTitleView != null) { 373 vh.mTitleView.setMaxLines(mTitleMinLines); 374 } 375 if (vh.mDescriptionView != null) { 376 vh.mDescriptionView.setMaxLines(mDescriptionMinLines); 377 } 378 } 379 } 380 381 /** 382 * Animates the view holder's view (or subviews thereof) when the action has had its focus 383 * state changed. 384 * @param vh The view holder associated with the relevant action. 385 * @param focused True if the action has become focused, false if it has lost focus. 386 */ 387 public void onAnimateItemFocused(ViewHolder vh, boolean focused) { 388 // No animations for this, currently, because the animation is done on 389 // mSelectorView 390 } 391 392 /** 393 * Animates the view holder's view (or subviews thereof) when the action has had its press 394 * state changed. 395 * @param vh The view holder associated with the relevant action. 396 * @param pressed True if the action has been pressed, false if it has been unpressed. 397 */ 398 public void onAnimateItemPressed(ViewHolder vh, boolean pressed) { 399 int attr = pressed ? R.attr.guidedActionPressedAnimation : 400 R.attr.guidedActionUnpressedAnimation; 401 createAnimator(vh.view, attr).start(); 402 } 403 404 /** 405 * Animates the view holder's view (or subviews thereof) when the action has had its check 406 * state changed. 407 * @param vh The view holder associated with the relevant action. 408 * @param checked True if the action has become checked, false if it has become unchecked. 409 */ 410 public void onAnimateItemChecked(ViewHolder vh, boolean checked) { 411 final View checkView = vh.mCheckmarkView; 412 if (checkView != null) { 413 if (checked) { 414 checkView.setVisibility(View.VISIBLE); 415 createAnimator(checkView, R.attr.guidedActionCheckedAnimation).start(); 416 } else { 417 Animator animator = createAnimator(checkView, R.attr.guidedActionCheckedAnimation); 418 animator.addListener(new AnimatorListenerAdapter() { 419 @Override 420 public void onAnimationEnd(Animator animation) { 421 checkView.setVisibility(View.INVISIBLE); 422 } 423 }); 424 animator.start(); 425 } 426 } 427 } 428 429 /* 430 * ========================================== 431 * FragmentAnimationProvider overrides 432 * ========================================== 433 */ 434 435 /** 436 * {@inheritDoc} 437 */ 438 @Override 439 public void onActivityEnter(@NonNull List<Animator> animators) { 440 animators.add(createAnimator(mMainView, R.attr.guidedActionsEntryAnimation)); 441 } 442 443 /** 444 * {@inheritDoc} 445 */ 446 @Override 447 public void onActivityExit(@NonNull List<Animator> animators) {} 448 449 /** 450 * {@inheritDoc} 451 */ 452 @Override 453 public void onFragmentEnter(@NonNull List<Animator> animators) { 454 animators.add(createAnimator(mActionsGridView, R.attr.guidedStepEntryAnimation)); 455 animators.add(createAnimator(mSelectorView, R.attr.guidedStepEntryAnimation)); 456 } 457 458 /** 459 * {@inheritDoc} 460 */ 461 @Override 462 public void onFragmentExit(@NonNull List<Animator> animators) { 463 animators.add(createAnimator(mActionsGridView, R.attr.guidedStepExitAnimation)); 464 animators.add(createAnimator(mSelectorView, R.attr.guidedStepExitAnimation)); 465 } 466 467 /** 468 * {@inheritDoc} 469 */ 470 @Override 471 public void onFragmentReenter(@NonNull List<Animator> animators) { 472 animators.add(createAnimator(mActionsGridView, R.attr.guidedStepReentryAnimation)); 473 animators.add(createAnimator(mSelectorView, R.attr.guidedStepReentryAnimation)); 474 } 475 476 /** 477 * {@inheritDoc} 478 */ 479 @Override 480 public void onFragmentReturn(@NonNull List<Animator> animators) { 481 animators.add(createAnimator(mActionsGridView, R.attr.guidedStepReturnAnimation)); 482 animators.add(createAnimator(mSelectorView, R.attr.guidedStepReturnAnimation)); 483 } 484 485 /* 486 * ========================================== 487 * Private methods 488 * ========================================== 489 */ 490 491 private void updateSelectorView(View focusedChild) { 492 // Display the selector view. 493 int height = focusedChild.getHeight(); 494 LayoutParams lp = mSelectorView.getLayoutParams(); 495 lp.height = height; 496 mSelectorView.setLayoutParams(lp); 497 mSelectorView.setAlpha(1f); 498 } 499 500 private float getFloat(Context ctx, TypedValue typedValue, int attrId) { 501 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 502 // Android resources don't have a native float type, so we have to use strings. 503 return Float.valueOf(ctx.getResources().getString(typedValue.resourceId)); 504 } 505 506 private int getInteger(Context ctx, TypedValue typedValue, int attrId) { 507 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 508 return ctx.getResources().getInteger(typedValue.resourceId); 509 } 510 511 private int getDimension(Context ctx, TypedValue typedValue, int attrId) { 512 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 513 return ctx.getResources().getDimensionPixelSize(typedValue.resourceId); 514 } 515 516 private static Animator createAnimator(View v, int attrId) { 517 Context ctx = v.getContext(); 518 TypedValue typedValue = new TypedValue(); 519 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 520 Animator animator = AnimatorInflater.loadAnimator(ctx, typedValue.resourceId); 521 animator.setTarget(v); 522 return animator; 523 } 524 525 private boolean setIcon(final ImageView iconView, GuidedAction action) { 526 Drawable icon = null; 527 if (iconView != null) { 528 Context context = iconView.getContext(); 529 icon = action.getIcon(); 530 if (icon != null) { 531 iconView.setImageDrawable(icon); 532 iconView.setVisibility(View.VISIBLE); 533 } else { 534 iconView.setVisibility(View.GONE); 535 } 536 } 537 return icon != null; 538 } 539 540 /** 541 * @return the max height in pixels the description can be such that the 542 * action nicely takes up the entire screen. 543 */ 544 private int getDescriptionMaxHeight(Context context, TextView title) { 545 // The 2 multiplier on the title height calculation is a 546 // conservative estimate for font padding which can not be 547 // calculated at this stage since the view hasn't been rendered yet. 548 return (int)(mDisplayHeight - 2*mVerticalPadding - 2*mTitleMaxLines*title.getLineHeight()); 549 } 550 551 /** 552 * SelectorAnimator 553 * Controls animation for selected item backgrounds 554 * TODO: Move into focus animation override? 555 */ 556 private static class SelectorAnimator extends RecyclerView.OnScrollListener { 557 558 private final View mSelectorView; 559 private final ViewGroup mParentView; 560 private volatile boolean mFadedOut = true; 561 562 SelectorAnimator(View selectorView, ViewGroup parentView) { 563 mSelectorView = selectorView; 564 mParentView = parentView; 565 } 566 567 // We want to fade in the selector if we've stopped scrolling on it. If 568 // we're scrolling, we want to ensure to dim the selector if we haven't 569 // already. We dim the last highlighted view so that while a user is 570 // scrolling, nothing is highlighted. 571 @Override 572 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 573 Animator animator = null; 574 boolean fadingOut = false; 575 if (newState == RecyclerView.SCROLL_STATE_IDLE) { 576 // The selector starts with a height of 0. In order to scale up from 577 // 0, we first need the set the height to 1 and scale from there. 578 View focusedChild = mParentView.getFocusedChild(); 579 if (focusedChild != null) { 580 int selectorHeight = mSelectorView.getHeight(); 581 float scaleY = (float) focusedChild.getHeight() / selectorHeight; 582 AnimatorSet animators = (AnimatorSet)createAnimator(mSelectorView, 583 R.attr.guidedActionsSelectorShowAnimation); 584 if (mFadedOut) { 585 // selector is completely faded out, so we can just scale before fading in. 586 mSelectorView.setScaleY(scaleY); 587 animator = animators.getChildAnimations().get(0); 588 } else { 589 // selector is not faded out, so we must animate the scale as we fade in. 590 ((ObjectAnimator)animators.getChildAnimations().get(1)) 591 .setFloatValues(scaleY); 592 animator = animators; 593 } 594 } 595 } else { 596 animator = createAnimator(mSelectorView, R.attr.guidedActionsSelectorHideAnimation); 597 fadingOut = true; 598 } 599 if (animator != null) { 600 animator.addListener(new Listener(fadingOut)); 601 animator.start(); 602 } 603 } 604 605 /** 606 * Sets {@link BaseScrollAdapterFragment#mFadedOut} 607 * {@link BaseScrollAdapterFragment#mFadedOut} is true, iff 608 * {@link BaseScrollAdapterFragment#mSelectorView} has an alpha of 0 609 * (faded out). If false the view either has an alpha of 1 (visible) or 610 * is in the process of animating. 611 */ 612 private class Listener implements Animator.AnimatorListener { 613 private boolean mFadingOut; 614 private boolean mCanceled; 615 616 public Listener(boolean fadingOut) { 617 mFadingOut = fadingOut; 618 } 619 620 @Override 621 public void onAnimationStart(Animator animation) { 622 if (!mFadingOut) { 623 mFadedOut = false; 624 } 625 } 626 627 @Override 628 public void onAnimationEnd(Animator animation) { 629 if (!mCanceled && mFadingOut) { 630 mFadedOut = true; 631 } 632 } 633 634 @Override 635 public void onAnimationCancel(Animator animation) { 636 mCanceled = true; 637 } 638 639 @Override 640 public void onAnimationRepeat(Animator animation) { 641 } 642 } 643 } 644 645} 646