1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.support.transition; 18 19 import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; 20 21 import android.animation.Animator; 22 import android.animation.AnimatorListenerAdapter; 23 import android.content.Context; 24 import android.content.res.TypedArray; 25 import android.content.res.XmlResourceParser; 26 import android.support.annotation.IntDef; 27 import android.support.annotation.NonNull; 28 import android.support.annotation.Nullable; 29 import android.support.annotation.RestrictTo; 30 import android.support.v4.content.res.TypedArrayUtils; 31 import android.util.AttributeSet; 32 import android.view.View; 33 import android.view.ViewGroup; 34 35 import java.lang.annotation.Retention; 36 import java.lang.annotation.RetentionPolicy; 37 38 /** 39 * This transition tracks changes to the visibility of target views in the 40 * start and end scenes. Visibility is determined not just by the 41 * {@link View#setVisibility(int)} state of views, but also whether 42 * views exist in the current view hierarchy. The class is intended to be a 43 * utility for subclasses such as {@link Fade}, which use this visibility 44 * information to determine the specific animations to run when visibility 45 * changes occur. Subclasses should implement one or both of the methods 46 * {@link #onAppear(ViewGroup, TransitionValues, int, TransitionValues, int)}, 47 * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)} or 48 * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)}, 49 * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}. 50 */ 51 public abstract class Visibility extends Transition { 52 53 static final String PROPNAME_VISIBILITY = "android:visibility:visibility"; 54 private static final String PROPNAME_PARENT = "android:visibility:parent"; 55 private static final String PROPNAME_SCREEN_LOCATION = "android:visibility:screenLocation"; 56 57 /** 58 * Mode used in {@link #setMode(int)} to make the transition 59 * operate on targets that are appearing. Maybe be combined with 60 * {@link #MODE_OUT} to target Visibility changes both in and out. 61 */ 62 public static final int MODE_IN = 0x1; 63 64 /** 65 * Mode used in {@link #setMode(int)} to make the transition 66 * operate on targets that are disappearing. Maybe be combined with 67 * {@link #MODE_IN} to target Visibility changes both in and out. 68 */ 69 public static final int MODE_OUT = 0x2; 70 71 /** @hide */ 72 @RestrictTo(LIBRARY_GROUP) 73 @IntDef(flag = true, value = {MODE_IN, MODE_OUT}) 74 @Retention(RetentionPolicy.SOURCE) 75 public @interface Mode { 76 } 77 78 private static final String[] sTransitionProperties = { 79 PROPNAME_VISIBILITY, 80 PROPNAME_PARENT, 81 }; 82 83 private static class VisibilityInfo { 84 boolean mVisibilityChange; 85 boolean mFadeIn; 86 int mStartVisibility; 87 int mEndVisibility; 88 ViewGroup mStartParent; 89 ViewGroup mEndParent; 90 } 91 92 private int mMode = MODE_IN | MODE_OUT; 93 Visibility()94 public Visibility() { 95 } 96 Visibility(Context context, AttributeSet attrs)97 public Visibility(Context context, AttributeSet attrs) { 98 super(context, attrs); 99 TypedArray a = context.obtainStyledAttributes(attrs, Styleable.VISIBILITY_TRANSITION); 100 @Mode 101 int mode = TypedArrayUtils.getNamedInt(a, (XmlResourceParser) attrs, 102 "transitionVisibilityMode", 103 Styleable.VisibilityTransition.TRANSITION_VISIBILITY_MODE, 0); 104 a.recycle(); 105 if (mode != 0) { 106 setMode(mode); 107 } 108 } 109 110 /** 111 * Changes the transition to support appearing and/or disappearing Views, depending 112 * on <code>mode</code>. 113 * 114 * @param mode The behavior supported by this transition, a combination of 115 * {@link #MODE_IN} and {@link #MODE_OUT}. 116 */ setMode(@ode int mode)117 public void setMode(@Mode int mode) { 118 if ((mode & ~(MODE_IN | MODE_OUT)) != 0) { 119 throw new IllegalArgumentException("Only MODE_IN and MODE_OUT flags are allowed"); 120 } 121 mMode = mode; 122 } 123 124 /** 125 * Returns whether appearing and/or disappearing Views are supported. 126 * 127 * @return whether appearing and/or disappearing Views are supported. A combination of 128 * {@link #MODE_IN} and {@link #MODE_OUT}. 129 */ 130 @Mode getMode()131 public int getMode() { 132 return mMode; 133 } 134 135 @Nullable 136 @Override getTransitionProperties()137 public String[] getTransitionProperties() { 138 return sTransitionProperties; 139 } 140 captureValues(TransitionValues transitionValues)141 private void captureValues(TransitionValues transitionValues) { 142 int visibility = transitionValues.view.getVisibility(); 143 transitionValues.values.put(PROPNAME_VISIBILITY, visibility); 144 transitionValues.values.put(PROPNAME_PARENT, transitionValues.view.getParent()); 145 int[] loc = new int[2]; 146 transitionValues.view.getLocationOnScreen(loc); 147 transitionValues.values.put(PROPNAME_SCREEN_LOCATION, loc); 148 } 149 150 @Override captureStartValues(@onNull TransitionValues transitionValues)151 public void captureStartValues(@NonNull TransitionValues transitionValues) { 152 captureValues(transitionValues); 153 } 154 155 @Override captureEndValues(@onNull TransitionValues transitionValues)156 public void captureEndValues(@NonNull TransitionValues transitionValues) { 157 captureValues(transitionValues); 158 } 159 160 /** 161 * Returns whether the view is 'visible' according to the given values 162 * object. This is determined by testing the same properties in the values 163 * object that are used to determine whether the object is appearing or 164 * disappearing in the {@link 165 * Transition#createAnimator(ViewGroup, TransitionValues, TransitionValues)} 166 * method. This method can be called by, for example, subclasses that want 167 * to know whether the object is visible in the same way that Visibility 168 * determines it for the actual animation. 169 * 170 * @param values The TransitionValues object that holds the information by 171 * which visibility is determined. 172 * @return True if the view reference by <code>values</code> is visible, 173 * false otherwise. 174 */ isVisible(TransitionValues values)175 public boolean isVisible(TransitionValues values) { 176 if (values == null) { 177 return false; 178 } 179 int visibility = (Integer) values.values.get(PROPNAME_VISIBILITY); 180 View parent = (View) values.values.get(PROPNAME_PARENT); 181 182 return visibility == View.VISIBLE && parent != null; 183 } 184 getVisibilityChangeInfo(TransitionValues startValues, TransitionValues endValues)185 private VisibilityInfo getVisibilityChangeInfo(TransitionValues startValues, 186 TransitionValues endValues) { 187 final VisibilityInfo visInfo = new VisibilityInfo(); 188 visInfo.mVisibilityChange = false; 189 visInfo.mFadeIn = false; 190 if (startValues != null && startValues.values.containsKey(PROPNAME_VISIBILITY)) { 191 visInfo.mStartVisibility = (Integer) startValues.values.get(PROPNAME_VISIBILITY); 192 visInfo.mStartParent = (ViewGroup) startValues.values.get(PROPNAME_PARENT); 193 } else { 194 visInfo.mStartVisibility = -1; 195 visInfo.mStartParent = null; 196 } 197 if (endValues != null && endValues.values.containsKey(PROPNAME_VISIBILITY)) { 198 visInfo.mEndVisibility = (Integer) endValues.values.get(PROPNAME_VISIBILITY); 199 visInfo.mEndParent = (ViewGroup) endValues.values.get(PROPNAME_PARENT); 200 } else { 201 visInfo.mEndVisibility = -1; 202 visInfo.mEndParent = null; 203 } 204 if (startValues != null && endValues != null) { 205 if (visInfo.mStartVisibility == visInfo.mEndVisibility 206 && visInfo.mStartParent == visInfo.mEndParent) { 207 return visInfo; 208 } else { 209 if (visInfo.mStartVisibility != visInfo.mEndVisibility) { 210 if (visInfo.mStartVisibility == View.VISIBLE) { 211 visInfo.mFadeIn = false; 212 visInfo.mVisibilityChange = true; 213 } else if (visInfo.mEndVisibility == View.VISIBLE) { 214 visInfo.mFadeIn = true; 215 visInfo.mVisibilityChange = true; 216 } 217 // no visibilityChange if going between INVISIBLE and GONE 218 } else /* if (visInfo.mStartParent != visInfo.mEndParent) */ { 219 if (visInfo.mEndParent == null) { 220 visInfo.mFadeIn = false; 221 visInfo.mVisibilityChange = true; 222 } else if (visInfo.mStartParent == null) { 223 visInfo.mFadeIn = true; 224 visInfo.mVisibilityChange = true; 225 } 226 } 227 } 228 } else if (startValues == null && visInfo.mEndVisibility == View.VISIBLE) { 229 visInfo.mFadeIn = true; 230 visInfo.mVisibilityChange = true; 231 } else if (endValues == null && visInfo.mStartVisibility == View.VISIBLE) { 232 visInfo.mFadeIn = false; 233 visInfo.mVisibilityChange = true; 234 } 235 return visInfo; 236 } 237 238 @Nullable 239 @Override createAnimator(@onNull ViewGroup sceneRoot, @Nullable TransitionValues startValues, @Nullable TransitionValues endValues)240 public Animator createAnimator(@NonNull ViewGroup sceneRoot, 241 @Nullable TransitionValues startValues, @Nullable TransitionValues endValues) { 242 VisibilityInfo visInfo = getVisibilityChangeInfo(startValues, endValues); 243 if (visInfo.mVisibilityChange 244 && (visInfo.mStartParent != null || visInfo.mEndParent != null)) { 245 if (visInfo.mFadeIn) { 246 return onAppear(sceneRoot, startValues, visInfo.mStartVisibility, 247 endValues, visInfo.mEndVisibility); 248 } else { 249 return onDisappear(sceneRoot, startValues, visInfo.mStartVisibility, 250 endValues, visInfo.mEndVisibility 251 ); 252 } 253 } 254 return null; 255 } 256 257 /** 258 * The default implementation of this method does nothing. Subclasses 259 * should override if they need to create an Animator when targets appear. 260 * The method should only be called by the Visibility class; it is 261 * not intended to be called from external classes. 262 * 263 * @param sceneRoot The root of the transition hierarchy 264 * @param startValues The target values in the start scene 265 * @param startVisibility The target visibility in the start scene 266 * @param endValues The target values in the end scene 267 * @param endVisibility The target visibility in the end scene 268 * @return An Animator to be started at the appropriate time in the 269 * overall transition for this scene change. A null value means no animation 270 * should be run. 271 */ 272 @SuppressWarnings("UnusedParameters") onAppear(ViewGroup sceneRoot, TransitionValues startValues, int startVisibility, TransitionValues endValues, int endVisibility)273 public Animator onAppear(ViewGroup sceneRoot, TransitionValues startValues, int startVisibility, 274 TransitionValues endValues, int endVisibility) { 275 if ((mMode & MODE_IN) != MODE_IN || endValues == null) { 276 return null; 277 } 278 if (startValues == null) { 279 View endParent = (View) endValues.view.getParent(); 280 TransitionValues startParentValues = getMatchedTransitionValues(endParent, 281 false); 282 TransitionValues endParentValues = getTransitionValues(endParent, false); 283 VisibilityInfo parentVisibilityInfo = 284 getVisibilityChangeInfo(startParentValues, endParentValues); 285 if (parentVisibilityInfo.mVisibilityChange) { 286 return null; 287 } 288 } 289 return onAppear(sceneRoot, endValues.view, startValues, endValues); 290 } 291 292 /** 293 * The default implementation of this method returns a null Animator. Subclasses should 294 * override this method to make targets appear with the desired transition. The 295 * method should only be called from 296 * {@link #onAppear(ViewGroup, TransitionValues, int, TransitionValues, int)}. 297 * 298 * @param sceneRoot The root of the transition hierarchy 299 * @param view The View to make appear. This will be in the target scene's View 300 * hierarchy 301 * and 302 * will be VISIBLE. 303 * @param startValues The target values in the start scene 304 * @param endValues The target values in the end scene 305 * @return An Animator to be started at the appropriate time in the 306 * overall transition for this scene change. A null value means no animation 307 * should be run. 308 */ onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues)309 public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, 310 TransitionValues endValues) { 311 return null; 312 } 313 314 /** 315 * The default implementation of this method does nothing. Subclasses 316 * should override if they need to create an Animator when targets disappear. 317 * The method should only be called by the Visibility class; it is 318 * not intended to be called from external classes. 319 * 320 * @param sceneRoot The root of the transition hierarchy 321 * @param startValues The target values in the start scene 322 * @param startVisibility The target visibility in the start scene 323 * @param endValues The target values in the end scene 324 * @param endVisibility The target visibility in the end scene 325 * @return An Animator to be started at the appropriate time in the 326 * overall transition for this scene change. A null value means no animation 327 * should be run. 328 */ 329 @SuppressWarnings("UnusedParameters") onDisappear(ViewGroup sceneRoot, TransitionValues startValues, int startVisibility, TransitionValues endValues, int endVisibility)330 public Animator onDisappear(ViewGroup sceneRoot, TransitionValues startValues, 331 int startVisibility, TransitionValues endValues, int endVisibility) { 332 if ((mMode & MODE_OUT) != MODE_OUT) { 333 return null; 334 } 335 336 View startView = (startValues != null) ? startValues.view : null; 337 View endView = (endValues != null) ? endValues.view : null; 338 View overlayView = null; 339 View viewToKeep = null; 340 if (endView == null || endView.getParent() == null) { 341 if (endView != null) { 342 // endView was removed from its parent - add it to the overlay 343 overlayView = endView; 344 } else if (startView != null) { 345 // endView does not exist. Use startView only under certain 346 // conditions, because placing a view in an overlay necessitates 347 // it being removed from its current parent 348 if (startView.getParent() == null) { 349 // no parent - safe to use 350 overlayView = startView; 351 } else if (startView.getParent() instanceof View) { 352 View startParent = (View) startView.getParent(); 353 TransitionValues startParentValues = getTransitionValues(startParent, true); 354 TransitionValues endParentValues = getMatchedTransitionValues(startParent, 355 true); 356 VisibilityInfo parentVisibilityInfo = 357 getVisibilityChangeInfo(startParentValues, endParentValues); 358 if (!parentVisibilityInfo.mVisibilityChange) { 359 overlayView = TransitionUtils.copyViewImage(sceneRoot, startView, 360 startParent); 361 } else if (startParent.getParent() == null) { 362 int id = startParent.getId(); 363 if (id != View.NO_ID && sceneRoot.findViewById(id) != null 364 && mCanRemoveViews) { 365 // no parent, but its parent is unparented but the parent 366 // hierarchy has been replaced by a new hierarchy with the same id 367 // and it is safe to un-parent startView 368 overlayView = startView; 369 } 370 } 371 } 372 } 373 } else { 374 // visibility change 375 if (endVisibility == View.INVISIBLE) { 376 viewToKeep = endView; 377 } else { 378 // Becoming GONE 379 if (startView == endView) { 380 viewToKeep = endView; 381 } else { 382 overlayView = startView; 383 } 384 } 385 } 386 final int finalVisibility = endVisibility; 387 388 if (overlayView != null && startValues != null) { 389 // TODO: Need to do this for general case of adding to overlay 390 int[] screenLoc = (int[]) startValues.values.get(PROPNAME_SCREEN_LOCATION); 391 int screenX = screenLoc[0]; 392 int screenY = screenLoc[1]; 393 int[] loc = new int[2]; 394 sceneRoot.getLocationOnScreen(loc); 395 overlayView.offsetLeftAndRight((screenX - loc[0]) - overlayView.getLeft()); 396 overlayView.offsetTopAndBottom((screenY - loc[1]) - overlayView.getTop()); 397 final ViewGroupOverlayImpl overlay = ViewGroupUtils.getOverlay(sceneRoot); 398 overlay.add(overlayView); 399 Animator animator = onDisappear(sceneRoot, overlayView, startValues, endValues); 400 if (animator == null) { 401 overlay.remove(overlayView); 402 } else { 403 final View finalOverlayView = overlayView; 404 animator.addListener(new AnimatorListenerAdapter() { 405 @Override 406 public void onAnimationEnd(Animator animation) { 407 overlay.remove(finalOverlayView); 408 } 409 }); 410 } 411 return animator; 412 } 413 414 if (viewToKeep != null) { 415 int originalVisibility = viewToKeep.getVisibility(); 416 ViewUtils.setTransitionVisibility(viewToKeep, View.VISIBLE); 417 Animator animator = onDisappear(sceneRoot, viewToKeep, startValues, endValues); 418 if (animator != null) { 419 DisappearListener disappearListener = new DisappearListener(viewToKeep, 420 finalVisibility, true); 421 animator.addListener(disappearListener); 422 AnimatorUtils.addPauseListener(animator, disappearListener); 423 addListener(disappearListener); 424 } else { 425 ViewUtils.setTransitionVisibility(viewToKeep, originalVisibility); 426 } 427 return animator; 428 } 429 return null; 430 } 431 432 /** 433 * The default implementation of this method returns a null Animator. Subclasses should 434 * override this method to make targets disappear with the desired transition. The 435 * method should only be called from 436 * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)}. 437 * 438 * @param sceneRoot The root of the transition hierarchy 439 * @param view The View to make disappear. This will be in the target scene's View 440 * hierarchy or in an {@link android.view.ViewGroupOverlay} and will be 441 * VISIBLE. 442 * @param startValues The target values in the start scene 443 * @param endValues The target values in the end scene 444 * @return An Animator to be started at the appropriate time in the 445 * overall transition for this scene change. A null value means no animation 446 * should be run. 447 */ onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues)448 public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, 449 TransitionValues endValues) { 450 return null; 451 } 452 453 @Override isTransitionRequired(TransitionValues startValues, TransitionValues newValues)454 public boolean isTransitionRequired(TransitionValues startValues, TransitionValues newValues) { 455 if (startValues == null && newValues == null) { 456 return false; 457 } 458 if (startValues != null && newValues != null 459 && newValues.values.containsKey(PROPNAME_VISIBILITY) 460 != startValues.values.containsKey(PROPNAME_VISIBILITY)) { 461 // The transition wasn't targeted in either the start or end, so it couldn't 462 // have changed. 463 return false; 464 } 465 VisibilityInfo changeInfo = getVisibilityChangeInfo(startValues, newValues); 466 return changeInfo.mVisibilityChange && (changeInfo.mStartVisibility == View.VISIBLE 467 || changeInfo.mEndVisibility == View.VISIBLE); 468 } 469 470 private static class DisappearListener extends AnimatorListenerAdapter 471 implements TransitionListener, AnimatorUtilsApi14.AnimatorPauseListenerCompat { 472 473 private final View mView; 474 private final int mFinalVisibility; 475 private final ViewGroup mParent; 476 private final boolean mSuppressLayout; 477 478 private boolean mLayoutSuppressed; 479 boolean mCanceled = false; 480 DisappearListener(View view, int finalVisibility, boolean suppressLayout)481 DisappearListener(View view, int finalVisibility, boolean suppressLayout) { 482 mView = view; 483 mFinalVisibility = finalVisibility; 484 mParent = (ViewGroup) view.getParent(); 485 mSuppressLayout = suppressLayout; 486 // Prevent a layout from including mView in its calculation. 487 suppressLayout(true); 488 } 489 490 // This overrides both AnimatorListenerAdapter and 491 // AnimatorUtilsApi14.AnimatorPauseListenerCompat 492 @Override onAnimationPause(Animator animation)493 public void onAnimationPause(Animator animation) { 494 if (!mCanceled) { 495 ViewUtils.setTransitionVisibility(mView, mFinalVisibility); 496 } 497 } 498 499 // This overrides both AnimatorListenerAdapter and 500 // AnimatorUtilsApi14.AnimatorPauseListenerCompat 501 @Override onAnimationResume(Animator animation)502 public void onAnimationResume(Animator animation) { 503 if (!mCanceled) { 504 ViewUtils.setTransitionVisibility(mView, View.VISIBLE); 505 } 506 } 507 508 @Override onAnimationCancel(Animator animation)509 public void onAnimationCancel(Animator animation) { 510 mCanceled = true; 511 } 512 513 @Override onAnimationRepeat(Animator animation)514 public void onAnimationRepeat(Animator animation) { 515 } 516 517 @Override onAnimationStart(Animator animation)518 public void onAnimationStart(Animator animation) { 519 } 520 521 @Override onAnimationEnd(Animator animation)522 public void onAnimationEnd(Animator animation) { 523 hideViewWhenNotCanceled(); 524 } 525 526 @Override onTransitionStart(@onNull Transition transition)527 public void onTransitionStart(@NonNull Transition transition) { 528 // Do nothing 529 } 530 531 @Override onTransitionEnd(@onNull Transition transition)532 public void onTransitionEnd(@NonNull Transition transition) { 533 hideViewWhenNotCanceled(); 534 transition.removeListener(this); 535 } 536 537 @Override onTransitionCancel(@onNull Transition transition)538 public void onTransitionCancel(@NonNull Transition transition) { 539 } 540 541 @Override onTransitionPause(@onNull Transition transition)542 public void onTransitionPause(@NonNull Transition transition) { 543 suppressLayout(false); 544 } 545 546 @Override onTransitionResume(@onNull Transition transition)547 public void onTransitionResume(@NonNull Transition transition) { 548 suppressLayout(true); 549 } 550 hideViewWhenNotCanceled()551 private void hideViewWhenNotCanceled() { 552 if (!mCanceled) { 553 // Recreate the parent's display list in case it includes mView. 554 ViewUtils.setTransitionVisibility(mView, mFinalVisibility); 555 if (mParent != null) { 556 mParent.invalidate(); 557 } 558 } 559 // Layout is allowed now that the View is in its final state 560 suppressLayout(false); 561 } 562 suppressLayout(boolean suppress)563 private void suppressLayout(boolean suppress) { 564 if (mSuppressLayout && mLayoutSuppressed != suppress && mParent != null) { 565 mLayoutSuppressed = suppress; 566 ViewGroupUtils.suppressLayout(mParent, suppress); 567 } 568 } 569 } 570 571 // TODO: Implement API 23; isTransitionRequired 572 573 } 574