1 /* 2 * Copyright (C) 2013 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.transition; 18 19 import android.content.Context; 20 import android.graphics.PointF; 21 22 import android.animation.Animator; 23 import android.animation.AnimatorListenerAdapter; 24 import android.animation.ObjectAnimator; 25 import android.animation.PropertyValuesHolder; 26 import android.animation.RectEvaluator; 27 import android.graphics.Bitmap; 28 import android.graphics.Canvas; 29 import android.graphics.Path; 30 import android.graphics.Rect; 31 import android.graphics.drawable.BitmapDrawable; 32 import android.graphics.drawable.Drawable; 33 import android.util.AttributeSet; 34 import android.util.IntProperty; 35 import android.util.Property; 36 import android.view.View; 37 import android.view.ViewGroup; 38 39 import java.util.Map; 40 41 /** 42 * This transition captures the layout bounds of target views before and after 43 * the scene change and animates those changes during the transition. 44 * 45 * <p>A ChangeBounds transition can be described in a resource file by using the 46 * tag <code>changeBounds</code>, along with the other standard 47 * attributes of {@link android.R.styleable#Transition}.</p> 48 */ 49 public class ChangeBounds extends Transition { 50 51 private static final String PROPNAME_BOUNDS = "android:changeBounds:bounds"; 52 private static final String PROPNAME_PARENT = "android:changeBounds:parent"; 53 private static final String PROPNAME_WINDOW_X = "android:changeBounds:windowX"; 54 private static final String PROPNAME_WINDOW_Y = "android:changeBounds:windowY"; 55 private static final String[] sTransitionProperties = { 56 PROPNAME_BOUNDS, 57 PROPNAME_PARENT, 58 PROPNAME_WINDOW_X, 59 PROPNAME_WINDOW_Y 60 }; 61 62 private static final Property<Drawable, PointF> DRAWABLE_ORIGIN_PROPERTY = 63 new Property<Drawable, PointF>(PointF.class, "boundsOrigin") { 64 private Rect mBounds = new Rect(); 65 66 @Override 67 public void set(Drawable object, PointF value) { 68 object.copyBounds(mBounds); 69 mBounds.offsetTo(Math.round(value.x), Math.round(value.y)); 70 object.setBounds(mBounds); 71 } 72 73 @Override 74 public PointF get(Drawable object) { 75 object.copyBounds(mBounds); 76 return new PointF(mBounds.left, mBounds.top); 77 } 78 }; 79 80 int[] tempLocation = new int[2]; 81 boolean mResizeClip = false; 82 boolean mReparent = false; 83 private static final String LOG_TAG = "ChangeBounds"; 84 85 private static RectEvaluator sRectEvaluator = new RectEvaluator(); 86 87 public ChangeBounds() {} 88 89 public ChangeBounds(Context context, AttributeSet attrs) { 90 super(context, attrs); 91 } 92 93 @Override 94 public String[] getTransitionProperties() { 95 return sTransitionProperties; 96 } 97 98 public void setResizeClip(boolean resizeClip) { 99 mResizeClip = resizeClip; 100 } 101 102 /** 103 * Setting this flag tells ChangeBounds to track the before/after parent 104 * of every view using this transition. The flag is not enabled by 105 * default because it requires the parent instances to be the same 106 * in the two scenes or else all parents must use ids to allow 107 * the transition to determine which parents are the same. 108 * 109 * @param reparent true if the transition should track the parent 110 * container of target views and animate parent changes. 111 * @deprecated Use {@link android.transition.ChangeTransform} to handle 112 * transitions between different parents. 113 */ 114 public void setReparent(boolean reparent) { 115 mReparent = reparent; 116 } 117 118 private void captureValues(TransitionValues values) { 119 View view = values.view; 120 121 if (view.isLaidOut() || view.getWidth() != 0 || view.getHeight() != 0) { 122 values.values.put(PROPNAME_BOUNDS, new Rect(view.getLeft(), view.getTop(), 123 view.getRight(), view.getBottom())); 124 values.values.put(PROPNAME_PARENT, values.view.getParent()); 125 if (mReparent) { 126 values.view.getLocationInWindow(tempLocation); 127 values.values.put(PROPNAME_WINDOW_X, tempLocation[0]); 128 values.values.put(PROPNAME_WINDOW_Y, tempLocation[1]); 129 } 130 } 131 } 132 133 @Override 134 public void captureStartValues(TransitionValues transitionValues) { 135 captureValues(transitionValues); 136 } 137 138 @Override 139 public void captureEndValues(TransitionValues transitionValues) { 140 captureValues(transitionValues); 141 } 142 143 private boolean parentMatches(View startParent, View endParent) { 144 boolean parentMatches = true; 145 if (mReparent) { 146 TransitionValues endValues = getMatchedTransitionValues(startParent, true); 147 if (endValues == null) { 148 parentMatches = startParent == endParent; 149 } else { 150 parentMatches = endParent == endValues.view; 151 } 152 } 153 return parentMatches; 154 } 155 156 @Override 157 public Animator createAnimator(final ViewGroup sceneRoot, TransitionValues startValues, 158 TransitionValues endValues) { 159 if (startValues == null || endValues == null) { 160 return null; 161 } 162 Map<String, Object> startParentVals = startValues.values; 163 Map<String, Object> endParentVals = endValues.values; 164 ViewGroup startParent = (ViewGroup) startParentVals.get(PROPNAME_PARENT); 165 ViewGroup endParent = (ViewGroup) endParentVals.get(PROPNAME_PARENT); 166 if (startParent == null || endParent == null) { 167 return null; 168 } 169 final View view = endValues.view; 170 if (parentMatches(startParent, endParent)) { 171 Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS); 172 Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS); 173 int startLeft = startBounds.left; 174 int endLeft = endBounds.left; 175 int startTop = startBounds.top; 176 int endTop = endBounds.top; 177 int startRight = startBounds.right; 178 int endRight = endBounds.right; 179 int startBottom = startBounds.bottom; 180 int endBottom = endBounds.bottom; 181 int startWidth = startRight - startLeft; 182 int startHeight = startBottom - startTop; 183 int endWidth = endRight - endLeft; 184 int endHeight = endBottom - endTop; 185 int numChanges = 0; 186 if ((startWidth != 0 && startHeight != 0) || (endWidth != 0 && endHeight != 0)) { 187 if (startLeft != endLeft || startTop != endTop) ++numChanges; 188 if (startRight != endRight || startBottom != endBottom) ++numChanges; 189 } 190 if (numChanges > 0) { 191 if (!mResizeClip) { 192 Animator anim; 193 if (startWidth == endWidth && startHeight == endHeight) { 194 view.offsetLeftAndRight(startLeft - view.getLeft()); 195 view.offsetTopAndBottom(startTop - view.getTop()); 196 Path positionPath = getPathMotion().getPath(0, 0, endLeft - startLeft, 197 endTop - startTop); 198 anim = ObjectAnimator.ofInt(view, new HorizontalOffsetProperty(), 199 new VerticalOffsetProperty(), positionPath); 200 } else { 201 if (startLeft != endLeft) view.setLeft(startLeft); 202 if (startTop != endTop) view.setTop(startTop); 203 if (startRight != endRight) view.setRight(startRight); 204 if (startBottom != endBottom) view.setBottom(startBottom); 205 ObjectAnimator topLeftAnimator = null; 206 if (startLeft != endLeft || startTop != endTop) { 207 Path topLeftPath = getPathMotion().getPath(startLeft, startTop, 208 endLeft, endTop); 209 topLeftAnimator = ObjectAnimator 210 .ofInt(view, "left", "top", topLeftPath); 211 } 212 ObjectAnimator bottomRightAnimator = null; 213 if (startRight != endRight || startBottom != endBottom) { 214 Path bottomRightPath = getPathMotion().getPath(startRight, startBottom, 215 endRight, endBottom); 216 bottomRightAnimator = ObjectAnimator.ofInt(view, "right", "bottom", 217 bottomRightPath); 218 } 219 anim = TransitionUtils.mergeAnimators(topLeftAnimator, 220 bottomRightAnimator); 221 } 222 if (view.getParent() instanceof ViewGroup) { 223 final ViewGroup parent = (ViewGroup) view.getParent(); 224 parent.suppressLayout(true); 225 TransitionListener transitionListener = new TransitionListenerAdapter() { 226 boolean mCanceled = false; 227 228 @Override 229 public void onTransitionCancel(Transition transition) { 230 parent.suppressLayout(false); 231 mCanceled = true; 232 } 233 234 @Override 235 public void onTransitionEnd(Transition transition) { 236 if (!mCanceled) { 237 parent.suppressLayout(false); 238 } 239 } 240 241 @Override 242 public void onTransitionPause(Transition transition) { 243 parent.suppressLayout(false); 244 } 245 246 @Override 247 public void onTransitionResume(Transition transition) { 248 parent.suppressLayout(true); 249 } 250 }; 251 addListener(transitionListener); 252 } 253 return anim; 254 } else { 255 if (startWidth != endWidth) view.setRight(endLeft + 256 Math.max(startWidth, endWidth)); 257 if (startHeight != endHeight) view.setBottom(endTop + 258 Math.max(startHeight, endHeight)); 259 // TODO: don't clobber TX/TY 260 if (startLeft != endLeft) view.setTranslationX(startLeft - endLeft); 261 if (startTop != endTop) view.setTranslationY(startTop - endTop); 262 // Animate location with translationX/Y and size with clip bounds 263 float transXDelta = endLeft - startLeft; 264 float transYDelta = endTop - startTop; 265 int widthDelta = endWidth - startWidth; 266 int heightDelta = endHeight - startHeight; 267 numChanges = 0; 268 if (transXDelta != 0) numChanges++; 269 if (transYDelta != 0) numChanges++; 270 if (widthDelta != 0 || heightDelta != 0) numChanges++; 271 ObjectAnimator translationAnimator = null; 272 if (transXDelta != 0 || transYDelta != 0) { 273 Path topLeftPath = getPathMotion().getPath(0, 0, transXDelta, transYDelta); 274 translationAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 275 View.TRANSLATION_Y, topLeftPath); 276 } 277 ObjectAnimator clipAnimator = null; 278 if (widthDelta != 0 || heightDelta != 0) { 279 Rect tempStartBounds = new Rect(0, 0, startWidth, startHeight); 280 Rect tempEndBounds = new Rect(0, 0, endWidth, endHeight); 281 clipAnimator = ObjectAnimator.ofObject(view, "clipBounds", sRectEvaluator, 282 tempStartBounds, tempEndBounds); 283 } 284 Animator anim = TransitionUtils.mergeAnimators(translationAnimator, 285 clipAnimator); 286 if (view.getParent() instanceof ViewGroup) { 287 final ViewGroup parent = (ViewGroup) view.getParent(); 288 parent.suppressLayout(true); 289 TransitionListener transitionListener = new TransitionListenerAdapter() { 290 boolean mCanceled = false; 291 292 @Override 293 public void onTransitionCancel(Transition transition) { 294 parent.suppressLayout(false); 295 mCanceled = true; 296 } 297 298 @Override 299 public void onTransitionEnd(Transition transition) { 300 if (!mCanceled) { 301 parent.suppressLayout(false); 302 } 303 } 304 305 @Override 306 public void onTransitionPause(Transition transition) { 307 parent.suppressLayout(false); 308 } 309 310 @Override 311 public void onTransitionResume(Transition transition) { 312 parent.suppressLayout(true); 313 } 314 }; 315 addListener(transitionListener); 316 } 317 anim.addListener(new AnimatorListenerAdapter() { 318 @Override 319 public void onAnimationEnd(Animator animation) { 320 view.setClipBounds(null); 321 } 322 }); 323 return anim; 324 } 325 } 326 } else { 327 int startX = (Integer) startValues.values.get(PROPNAME_WINDOW_X); 328 int startY = (Integer) startValues.values.get(PROPNAME_WINDOW_Y); 329 int endX = (Integer) endValues.values.get(PROPNAME_WINDOW_X); 330 int endY = (Integer) endValues.values.get(PROPNAME_WINDOW_Y); 331 // TODO: also handle size changes: check bounds and animate size changes 332 if (startX != endX || startY != endY) { 333 sceneRoot.getLocationInWindow(tempLocation); 334 Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), 335 Bitmap.Config.ARGB_8888); 336 Canvas canvas = new Canvas(bitmap); 337 view.draw(canvas); 338 final BitmapDrawable drawable = new BitmapDrawable(bitmap); 339 final float transitionAlpha = view.getTransitionAlpha(); 340 view.setTransitionAlpha(0); 341 sceneRoot.getOverlay().add(drawable); 342 Path topLeftPath = getPathMotion().getPath(startX - tempLocation[0], 343 startY - tempLocation[1], endX - tempLocation[0], endY - tempLocation[1]); 344 PropertyValuesHolder origin = PropertyValuesHolder.ofObject( 345 DRAWABLE_ORIGIN_PROPERTY, null, topLeftPath); 346 ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(drawable, origin); 347 anim.addListener(new AnimatorListenerAdapter() { 348 @Override 349 public void onAnimationEnd(Animator animation) { 350 sceneRoot.getOverlay().remove(drawable); 351 view.setTransitionAlpha(transitionAlpha); 352 } 353 }); 354 return anim; 355 } 356 } 357 return null; 358 } 359 360 private abstract static class OffsetProperty extends IntProperty<View> { 361 int mPreviousValue; 362 363 public OffsetProperty(String name) { 364 super(name); 365 } 366 367 @Override 368 public void setValue(View view, int value) { 369 int offset = value - mPreviousValue; 370 offsetBy(view, offset); 371 mPreviousValue = value; 372 } 373 374 @Override 375 public Integer get(View object) { 376 return null; 377 } 378 379 protected abstract void offsetBy(View view, int by); 380 } 381 382 private static class HorizontalOffsetProperty extends OffsetProperty { 383 public HorizontalOffsetProperty() { 384 super("offsetLeftAndRight"); 385 } 386 387 @Override 388 protected void offsetBy(View view, int by) { 389 view.offsetLeftAndRight(by); 390 } 391 } 392 393 private static class VerticalOffsetProperty extends OffsetProperty { 394 public VerticalOffsetProperty() { 395 super("offsetTopAndBottom"); 396 } 397 398 @Override 399 protected void offsetBy(View view, int by) { 400 view.offsetTopAndBottom(by); 401 } 402 } 403 } 404