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