GuidedActionsStylist.java revision d14724d33d61385c27a00c31bbc67ad8eeb57b3c
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_guidedActionsSelectorStyle 95 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsListStyle 96 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemContainerStyle 97 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemCheckmarkStyle 98 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemIconStyle 99 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemContentStyle 100 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemTitleStyle 101 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemDescriptionStyle 102 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemChevronStyle 103 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionCheckedAnimation 104 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionUncheckedAnimation 105 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionPressedAnimation 106 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionUnpressedAnimation 107 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionEnabledChevronAlpha 108 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionDisabledChevronAlpha 109 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionTitleMinLines 110 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionTitleMaxLines 111 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionDescriptionMinLines 112 * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionVerticalPadding 113 * @see android.support.v17.leanback.app.GuidedStepFragment 114 * @see GuidedAction 115 */ 116public class GuidedActionsStylist implements FragmentAnimationProvider { 117 118 /** 119 * Default viewType that associated with default layout Id for the action item. 120 * @see #getItemViewType(GuidedAction) 121 * @see #onProvideItemLayoutId(int) 122 * @see #onCreateViewHolder(ViewGroup, int) 123 */ 124 public static final int VIEW_TYPE_DEFAULT = 0; 125 126 /** 127 * ViewHolder caches information about the action item layouts' subviews. Subclasses of {@link 128 * GuidedActionsStylist} may also wish to subclass this in order to add fields. 129 * @see GuidedAction 130 */ 131 public static class ViewHolder { 132 133 public final View view; 134 135 private View mContentView; 136 private TextView mTitleView; 137 private TextView mDescriptionView; 138 private ImageView mIconView; 139 private ImageView mCheckmarkView; 140 private ImageView mChevronView; 141 private boolean mInEditing; 142 private boolean mInEditingDescription; 143 144 /** 145 * Constructs an ViewHolder and caches the relevant subviews. 146 */ 147 public ViewHolder(View v) { 148 view = v; 149 150 mContentView = v.findViewById(R.id.guidedactions_item_content); 151 mTitleView = (TextView) v.findViewById(R.id.guidedactions_item_title); 152 mDescriptionView = (TextView) v.findViewById(R.id.guidedactions_item_description); 153 mIconView = (ImageView) v.findViewById(R.id.guidedactions_item_icon); 154 mCheckmarkView = (ImageView) v.findViewById(R.id.guidedactions_item_checkmark); 155 mChevronView = (ImageView) v.findViewById(R.id.guidedactions_item_chevron); 156 } 157 158 /** 159 * Returns the content view within this view holder's view, where title and description are 160 * shown. 161 */ 162 public View getContentView() { 163 return mContentView; 164 } 165 166 /** 167 * Returns the title view within this view holder's view. 168 */ 169 public TextView getTitleView() { 170 return mTitleView; 171 } 172 173 /** 174 * Convenience method to return an editable version of the title, if possible, 175 * or null if the title view isn't an EditText. 176 */ 177 public EditText getEditableTitleView() { 178 return (mTitleView instanceof EditText) ? (EditText)mTitleView : null; 179 } 180 181 /** 182 * Returns the description view within this view holder's view. 183 */ 184 public TextView getDescriptionView() { 185 return mDescriptionView; 186 } 187 188 /** 189 * Convenience method to return an editable version of the description, if possible, 190 * or null if the description view isn't an EditText. 191 */ 192 public EditText getEditableDescriptionView() { 193 return (mDescriptionView instanceof EditText) ? (EditText)mDescriptionView : null; 194 } 195 196 /** 197 * Returns the icon view within this view holder's view. 198 */ 199 public ImageView getIconView() { 200 return mIconView; 201 } 202 203 /** 204 * Returns the checkmark view within this view holder's view. 205 */ 206 public ImageView getCheckmarkView() { 207 return mCheckmarkView; 208 } 209 210 /** 211 * Returns the chevron view within this view holder's view. 212 */ 213 public ImageView getChevronView() { 214 return mChevronView; 215 } 216 217 /** 218 * Returns true if the TextView is in editing title or description, false otherwise. 219 */ 220 public boolean isInEditing() { 221 return mInEditing; 222 } 223 224 /** 225 * Returns true if the TextView is in editing description, false otherwise. 226 */ 227 public boolean isInEditingDescription() { 228 return mInEditingDescription; 229 } 230 231 public View getEditingView() { 232 if (mInEditing) { 233 return mInEditingDescription ? mDescriptionView : mTitleView; 234 } else { 235 return null; 236 } 237 } 238 } 239 240 private static String TAG = "GuidedActionsStylist"; 241 242 protected View mMainView; 243 protected VerticalGridView mActionsGridView; 244 protected View mSelectorView; 245 246 // Cached values from resources 247 private float mEnabledTextAlpha; 248 private float mDisabledTextAlpha; 249 private float mEnabledDescriptionAlpha; 250 private float mDisabledDescriptionAlpha; 251 private float mEnabledChevronAlpha; 252 private float mDisabledChevronAlpha; 253 private int mTitleMinLines; 254 private int mTitleMaxLines; 255 private int mDescriptionMinLines; 256 private int mVerticalPadding; 257 private int mDisplayHeight; 258 259 /** 260 * Creates a view appropriate for displaying a list of GuidedActions, using the provided 261 * inflater and container. 262 * <p> 263 * <i>Note: Does not actually add the created view to the container; the caller should do 264 * this.</i> 265 * @param inflater The layout inflater to be used when constructing the view. 266 * @param container The view group to be passed in the call to 267 * <code>LayoutInflater.inflate</code>. 268 * @return The view to be added to the caller's view hierarchy. 269 */ 270 public View onCreateView(LayoutInflater inflater, ViewGroup container) { 271 mMainView = inflater.inflate(onProvideLayoutId(), container, false); 272 mSelectorView = mMainView.findViewById(R.id.guidedactions_selector); 273 if (mMainView instanceof VerticalGridView) { 274 mActionsGridView = (VerticalGridView) mMainView; 275 } else { 276 mActionsGridView = (VerticalGridView) mMainView.findViewById(R.id.guidedactions_list); 277 if (mActionsGridView == null) { 278 throw new IllegalStateException("No ListView exists."); 279 } 280 mActionsGridView.setWindowAlignmentOffset(0); 281 mActionsGridView.setWindowAlignmentOffsetPercent(50f); 282 mActionsGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); 283 if (mSelectorView != null) { 284 mActionsGridView.setOnScrollListener(new 285 SelectorAnimator(mSelectorView, mActionsGridView)); 286 } 287 } 288 289 if (mSelectorView != null) { 290 // ALlow focus to move to other views 291 mActionsGridView.getViewTreeObserver().addOnGlobalFocusChangeListener( 292 mGlobalFocusChangeListener); 293 } 294 295 // Cache widths, chevron alpha values, max and min text lines, etc 296 Context ctx = mMainView.getContext(); 297 TypedValue val = new TypedValue(); 298 mEnabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionEnabledChevronAlpha); 299 mDisabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionDisabledChevronAlpha); 300 mTitleMinLines = getInteger(ctx, val, R.attr.guidedActionTitleMinLines); 301 mTitleMaxLines = getInteger(ctx, val, R.attr.guidedActionTitleMaxLines); 302 mDescriptionMinLines = getInteger(ctx, val, R.attr.guidedActionDescriptionMinLines); 303 mVerticalPadding = getDimension(ctx, val, R.attr.guidedActionVerticalPadding); 304 mDisplayHeight = ((WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE)) 305 .getDefaultDisplay().getHeight(); 306 307 mEnabledTextAlpha = Float.valueOf(ctx.getResources().getString(R.string 308 .lb_guidedactions_item_unselected_text_alpha)); 309 mDisabledTextAlpha = Float.valueOf(ctx.getResources().getString(R.string 310 .lb_guidedactions_item_disabled_text_alpha)); 311 mEnabledDescriptionAlpha = Float.valueOf(ctx.getResources().getString(R.string 312 .lb_guidedactions_item_unselected_description_text_alpha)); 313 mDisabledDescriptionAlpha = Float.valueOf(ctx.getResources().getString(R.string 314 .lb_guidedactions_item_disabled_description_text_alpha)); 315 return mMainView; 316 } 317 318 final ViewTreeObserver.OnGlobalFocusChangeListener mGlobalFocusChangeListener = 319 new ViewTreeObserver.OnGlobalFocusChangeListener() { 320 private boolean mChildFocused; 321 322 @Override 323 public void onGlobalFocusChanged(View oldFocus, View newFocus) { 324 final View focusedChild = mActionsGridView.getFocusedChild(); 325 if (focusedChild == null) { 326 mSelectorView.setVisibility(View.INVISIBLE); 327 mChildFocused = false; 328 } else if (!mChildFocused) { 329 mChildFocused = true; 330 mSelectorView.setVisibility(View.VISIBLE); 331 // Change Selector size in a post Runnable, doing it directly in 332 // GlobalFocusChangeListener does not trigger the layout pass. 333 mSelectorView.post(new Runnable() { 334 public void run() { 335 int height = focusedChild.getHeight(); 336 LayoutParams lp = mSelectorView.getLayoutParams(); 337 lp.height = height; 338 mSelectorView.setLayoutParams(lp); 339 } 340 }); 341 } 342 } 343 }; 344 345 /** 346 * Called when destroy the View created by GuidedActionsStylist. 347 */ 348 public void onDestroyView() { 349 if (mSelectorView != null) { 350 mActionsGridView.getViewTreeObserver().removeOnGlobalFocusChangeListener( 351 mGlobalFocusChangeListener); 352 } 353 } 354 355 /** 356 * Returns the VerticalGridView that displays the list of GuidedActions. 357 * @return The VerticalGridView for this presenter. 358 */ 359 public VerticalGridView getActionsGridView() { 360 return mActionsGridView; 361 } 362 363 /** 364 * Provides the resource ID of the layout defining the host view for the list of guided actions. 365 * Subclasses may override to provide their own customized layouts. The base implementation 366 * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions}. If overridden, the 367 * substituted layout should contain matching IDs for any views that should be managed by the 368 * base class; this can be achieved by starting with a copy of the base layout file. 369 * @return The resource ID of the layout to be inflated to define the host view for the list 370 * of GuidedActions. 371 */ 372 public int onProvideLayoutId() { 373 return R.layout.lb_guidedactions; 374 } 375 376 /** 377 * Return view type of action, each different type can have differently associated layout Id. 378 * Default implementation returns {@link #VIEW_TYPE_DEFAULT}. 379 * @param action The action object. 380 * @return View type that used in {@link #onProvideItemLayoutId(int)}. 381 */ 382 public int getItemViewType(GuidedAction action) { 383 return VIEW_TYPE_DEFAULT; 384 } 385 386 /** 387 * Provides the resource ID of the layout defining the view for an individual guided actions. 388 * Subclasses may override to provide their own customized layouts. The base implementation 389 * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions_item}. If overridden, 390 * the substituted layout should contain matching IDs for any views that should be managed by 391 * the base class; this can be achieved by starting with a copy of the base layout file. Note 392 * that in order for the item to support editing, the title view should both subclass {@link 393 * android.widget.EditText} and implement {@link ImeKeyMonitor}; see {@link 394 * GuidedActionEditText}. To support different types of Layouts, override {@link 395 * #onProvideItemLayoutId(int)}. 396 * @return The resource ID of the layout to be inflated to define the view to display an 397 * individual GuidedAction. 398 */ 399 public int onProvideItemLayoutId() { 400 return R.layout.lb_guidedactions_item; 401 } 402 403 /** 404 * Provides the resource ID of the layout defining the view for an individual guided actions. 405 * Subclasses may override to provide their own customized layouts. The base implementation 406 * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions_item}. If overridden, 407 * the substituted layout should contain matching IDs for any views that should be managed by 408 * the base class; this can be achieved by starting with a copy of the base layout file. Note 409 * that in order for the item to support editing, the title view should both subclass {@link 410 * android.widget.EditText} and implement {@link ImeKeyMonitor}; see {@link 411 * GuidedActionEditText}. 412 * @param viewType View type returned by {@link #getItemViewType(GuidedAction)} 413 * @return The resource ID of the layout to be inflated to define the view to display an 414 * individual GuidedAction. 415 */ 416 public int onProvideItemLayoutId(int viewType) { 417 if (viewType == VIEW_TYPE_DEFAULT) { 418 return onProvideItemLayoutId(); 419 } else { 420 throw new RuntimeException("ViewType " + viewType + 421 " not supported in GuidedActionsStylist"); 422 } 423 } 424 425 /** 426 * Constructs a {@link ViewHolder} capable of representing {@link GuidedAction}s. Subclasses 427 * may choose to return a subclass of ViewHolder. To support different view types, override 428 * {@link #onCreateViewHolder(ViewGroup, int)} 429 * <p> 430 * <i>Note: Should not actually add the created view to the parent; the caller will do 431 * this.</i> 432 * @param parent The view group to be used as the parent of the new view. 433 * @return The view to be added to the caller's view hierarchy. 434 */ 435 public ViewHolder onCreateViewHolder(ViewGroup parent) { 436 LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 437 View v = inflater.inflate(onProvideItemLayoutId(), parent, false); 438 return new ViewHolder(v); 439 } 440 441 /** 442 * Constructs a {@link ViewHolder} capable of representing {@link GuidedAction}s. Subclasses 443 * may choose to return a subclass of ViewHolder. 444 * <p> 445 * <i>Note: Should not actually add the created view to the parent; the caller will do 446 * this.</i> 447 * @param parent The view group to be used as the parent of the new view. 448 * @param viewType The viewType returned by {@link #getItemViewType(GuidedAction)} 449 * @return The view to be added to the caller's view hierarchy. 450 */ 451 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 452 if (viewType == VIEW_TYPE_DEFAULT) { 453 return onCreateViewHolder(parent); 454 } 455 LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 456 View v = inflater.inflate(onProvideItemLayoutId(viewType), parent, false); 457 return new ViewHolder(v); 458 } 459 460 /** 461 * Binds a {@link ViewHolder} to a particular {@link GuidedAction}. 462 * @param vh The view holder to be associated with the given action. 463 * @param action The guided action to be displayed by the view holder's view. 464 * @return The view to be added to the caller's view hierarchy. 465 */ 466 public void onBindViewHolder(ViewHolder vh, GuidedAction action) { 467 468 if (vh.mTitleView != null) { 469 vh.mTitleView.setText(action.getTitle()); 470 vh.mTitleView.setAlpha(action.isEnabled() ? mEnabledTextAlpha : mDisabledTextAlpha); 471 } 472 if (vh.mDescriptionView != null) { 473 vh.mDescriptionView.setText(action.getDescription()); 474 vh.mDescriptionView.setVisibility(TextUtils.isEmpty(action.getDescription()) ? 475 View.GONE : View.VISIBLE); 476 vh.mDescriptionView.setAlpha(action.isEnabled() ? mEnabledDescriptionAlpha : 477 mDisabledDescriptionAlpha); 478 } 479 // Clients might want the check mark view to be gone entirely, in which case, ignore it. 480 if (vh.mCheckmarkView != null && vh.mCheckmarkView.getVisibility() != View.GONE) { 481 vh.mCheckmarkView.setVisibility(action.isChecked() ? View.VISIBLE : View.INVISIBLE); 482 } 483 484 if (vh.mChevronView != null) { 485 vh.mChevronView.setVisibility(action.hasNext() ? View.VISIBLE : View.GONE); 486 vh.mChevronView.setAlpha(action.isEnabled() ? mEnabledChevronAlpha : 487 mDisabledChevronAlpha); 488 } 489 490 if (action.hasMultilineDescription()) { 491 if (vh.mTitleView != null) { 492 vh.mTitleView.setMaxLines(mTitleMaxLines); 493 if (vh.mDescriptionView != null) { 494 vh.mDescriptionView.setMaxHeight(getDescriptionMaxHeight(vh.view.getContext(), 495 vh.mTitleView)); 496 } 497 } 498 } else { 499 if (vh.mTitleView != null) { 500 vh.mTitleView.setMaxLines(mTitleMinLines); 501 } 502 if (vh.mDescriptionView != null) { 503 vh.mDescriptionView.setMaxLines(mDescriptionMinLines); 504 } 505 } 506 setEditingMode(vh, action, false); 507 if (action.isFocusable()) { 508 vh.view.setFocusable(true); 509 if (vh.view instanceof ViewGroup) { 510 ((ViewGroup) vh.view).setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); 511 } 512 } else { 513 vh.view.setFocusable(false); 514 if (vh.view instanceof ViewGroup) { 515 ((ViewGroup) vh.view).setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); 516 } 517 } 518 } 519 520 public void setEditingMode(ViewHolder vh, GuidedAction action, boolean editing) { 521 if (editing != vh.mInEditing) { 522 vh.mInEditing = editing; 523 onEditingModeChange(vh, action, editing); 524 } 525 } 526 527 protected void onEditingModeChange(ViewHolder vh, GuidedAction action, boolean editing) { 528 TextView titleView = vh.getTitleView(); 529 TextView descriptionView = vh.getDescriptionView(); 530 if (editing) { 531 CharSequence editTitle = action.getEditTitle(); 532 if (titleView != null && editTitle != null) { 533 titleView.setText(editTitle); 534 } 535 CharSequence editDescription = action.getEditDescription(); 536 if (descriptionView != null && editDescription != null) { 537 descriptionView.setText(editDescription); 538 } 539 if (action.isDescriptionEditable()) { 540 if (descriptionView != null) { 541 descriptionView.setVisibility(View.VISIBLE); 542 descriptionView.setInputType(action.getDescriptionEditInputType()); 543 } 544 vh.mInEditingDescription = true; 545 } else { 546 vh.mInEditingDescription = false; 547 if (titleView != null) { 548 titleView.setInputType(action.getEditInputType()); 549 } 550 } 551 } else { 552 if (titleView != null) { 553 titleView.setText(action.getTitle()); 554 } 555 if (descriptionView != null) { 556 descriptionView.setText(action.getDescription()); 557 } 558 if (vh.mInEditingDescription) { 559 if (descriptionView != null) { 560 descriptionView.setVisibility(TextUtils.isEmpty(action.getDescription()) ? 561 View.GONE : View.VISIBLE); 562 descriptionView.setInputType(action.getDescriptionInputType()); 563 } 564 vh.mInEditingDescription = false; 565 } else { 566 if (titleView != null) { 567 titleView.setInputType(action.getInputType()); 568 } 569 } 570 } 571 } 572 573 /** 574 * Animates the view holder's view (or subviews thereof) when the action has had its focus 575 * state changed. 576 * @param vh The view holder associated with the relevant action. 577 * @param focused True if the action has become focused, false if it has lost focus. 578 */ 579 public void onAnimateItemFocused(ViewHolder vh, boolean focused) { 580 // No animations for this, currently, because the animation is done on 581 // mSelectorView 582 } 583 584 /** 585 * Animates the view holder's view (or subviews thereof) when the action has had its press 586 * state changed. 587 * @param vh The view holder associated with the relevant action. 588 * @param pressed True if the action has been pressed, false if it has been unpressed. 589 */ 590 public void onAnimateItemPressed(ViewHolder vh, boolean pressed) { 591 int attr = pressed ? R.attr.guidedActionPressedAnimation : 592 R.attr.guidedActionUnpressedAnimation; 593 createAnimator(vh.view, attr).start(); 594 } 595 596 /** 597 * Animates the view holder's view (or subviews thereof) when the action has had its check 598 * state changed. 599 * @param vh The view holder associated with the relevant action. 600 * @param checked True if the action has become checked, false if it has become unchecked. 601 */ 602 public void onAnimateItemChecked(ViewHolder vh, boolean checked) { 603 final View checkView = vh.mCheckmarkView; 604 if (checkView != null) { 605 if (checked) { 606 checkView.setVisibility(View.VISIBLE); 607 createAnimator(checkView, R.attr.guidedActionCheckedAnimation).start(); 608 } else { 609 Animator animator = createAnimator(checkView, 610 R.attr.guidedActionUncheckedAnimation); 611 animator.addListener(new AnimatorListenerAdapter() { 612 @Override 613 public void onAnimationEnd(Animator animation) { 614 checkView.setVisibility(View.INVISIBLE); 615 } 616 }); 617 animator.start(); 618 } 619 } 620 } 621 622 /* 623 * ========================================== 624 * FragmentAnimationProvider overrides 625 * ========================================== 626 */ 627 628 /** 629 * {@inheritDoc} 630 */ 631 @Override 632 public void onImeAppearing(@NonNull List<Animator> animators) { 633 animators.add(createAnimator(mActionsGridView, R.attr.guidedStepImeAppearingAnimation)); 634 animators.add(createAnimator(mSelectorView, R.attr.guidedStepImeAppearingAnimation)); 635 } 636 637 /** 638 * {@inheritDoc} 639 */ 640 @Override 641 public void onImeDisappearing(@NonNull List<Animator> animators) { 642 animators.add(createAnimator(mActionsGridView, R.attr.guidedStepImeDisappearingAnimation)); 643 animators.add(createAnimator(mSelectorView, R.attr.guidedStepImeDisappearingAnimation)); 644 } 645 646 /* 647 * ========================================== 648 * Private methods 649 * ========================================== 650 */ 651 652 private float getFloat(Context ctx, TypedValue typedValue, int attrId) { 653 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 654 // Android resources don't have a native float type, so we have to use strings. 655 return Float.valueOf(ctx.getResources().getString(typedValue.resourceId)); 656 } 657 658 private int getInteger(Context ctx, TypedValue typedValue, int attrId) { 659 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 660 return ctx.getResources().getInteger(typedValue.resourceId); 661 } 662 663 private int getDimension(Context ctx, TypedValue typedValue, int attrId) { 664 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 665 return ctx.getResources().getDimensionPixelSize(typedValue.resourceId); 666 } 667 668 private static Animator createAnimator(View v, int attrId) { 669 Context ctx = v.getContext(); 670 TypedValue typedValue = new TypedValue(); 671 ctx.getTheme().resolveAttribute(attrId, typedValue, true); 672 Animator animator = AnimatorInflater.loadAnimator(ctx, typedValue.resourceId); 673 animator.setTarget(v); 674 return animator; 675 } 676 677 private boolean setIcon(final ImageView iconView, GuidedAction action) { 678 Drawable icon = null; 679 if (iconView != null) { 680 Context context = iconView.getContext(); 681 icon = action.getIcon(); 682 if (icon != null) { 683 // setImageDrawable resets the drawable's level unless we set the view level first. 684 iconView.setImageLevel(icon.getLevel()); 685 iconView.setImageDrawable(icon); 686 iconView.setVisibility(View.VISIBLE); 687 } else { 688 iconView.setVisibility(View.GONE); 689 } 690 } 691 return icon != null; 692 } 693 694 /** 695 * @return the max height in pixels the description can be such that the 696 * action nicely takes up the entire screen. 697 */ 698 private int getDescriptionMaxHeight(Context context, TextView title) { 699 // The 2 multiplier on the title height calculation is a 700 // conservative estimate for font padding which can not be 701 // calculated at this stage since the view hasn't been rendered yet. 702 return (int)(mDisplayHeight - 2*mVerticalPadding - 2*mTitleMaxLines*title.getLineHeight()); 703 } 704 705 /** 706 * SelectorAnimator 707 * Controls animation for selected item backgrounds 708 * TODO: Move into focus animation override? 709 */ 710 private static class SelectorAnimator extends RecyclerView.OnScrollListener { 711 712 private final View mSelectorView; 713 private final ViewGroup mParentView; 714 private volatile boolean mFadedOut = true; 715 716 SelectorAnimator(View selectorView, ViewGroup parentView) { 717 mSelectorView = selectorView; 718 mParentView = parentView; 719 } 720 721 // We want to fade in the selector if we've stopped scrolling on it. If 722 // we're scrolling, we want to ensure to dim the selector if we haven't 723 // already. We dim the last highlighted view so that while a user is 724 // scrolling, nothing is highlighted. 725 @Override 726 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 727 Animator animator = null; 728 boolean fadingOut = false; 729 if (newState == RecyclerView.SCROLL_STATE_IDLE) { 730 // The selector starts with a height of 0. In order to scale up from 731 // 0, we first need the set the height to 1 and scale from there. 732 View focusedChild = mParentView.getFocusedChild(); 733 if (focusedChild != null) { 734 int selectorHeight = mSelectorView.getHeight(); 735 float scaleY = (float) focusedChild.getHeight() / selectorHeight; 736 AnimatorSet animators = (AnimatorSet)createAnimator(mSelectorView, 737 R.attr.guidedActionsSelectorShowAnimation); 738 if (mFadedOut) { 739 // selector is completely faded out, so we can just scale before fading in. 740 mSelectorView.setScaleY(scaleY); 741 animator = animators.getChildAnimations().get(0); 742 } else { 743 // selector is not faded out, so we must animate the scale as we fade in. 744 ((ObjectAnimator)animators.getChildAnimations().get(1)) 745 .setFloatValues(scaleY); 746 animator = animators; 747 } 748 } 749 } else { 750 animator = createAnimator(mSelectorView, R.attr.guidedActionsSelectorHideAnimation); 751 fadingOut = true; 752 } 753 if (animator != null) { 754 animator.addListener(new Listener(fadingOut)); 755 animator.start(); 756 } 757 } 758 759 /** 760 * Sets {@link BaseScrollAdapterFragment#mFadedOut} 761 * {@link BaseScrollAdapterFragment#mFadedOut} is true, iff 762 * {@link BaseScrollAdapterFragment#mSelectorView} has an alpha of 0 763 * (faded out). If false the view either has an alpha of 1 (visible) or 764 * is in the process of animating. 765 */ 766 private class Listener implements Animator.AnimatorListener { 767 private boolean mFadingOut; 768 private boolean mCanceled; 769 770 public Listener(boolean fadingOut) { 771 mFadingOut = fadingOut; 772 } 773 774 @Override 775 public void onAnimationStart(Animator animation) { 776 if (!mFadingOut) { 777 mFadedOut = false; 778 } 779 } 780 781 @Override 782 public void onAnimationEnd(Animator animation) { 783 if (!mCanceled && mFadingOut) { 784 mFadedOut = true; 785 } 786 } 787 788 @Override 789 public void onAnimationCancel(Animator animation) { 790 mCanceled = true; 791 } 792 793 @Override 794 public void onAnimationRepeat(Animator animation) { 795 } 796 } 797 } 798 799} 800