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