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