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