RenderSessionImpl.java revision 70552fb92dbc5cb5b1d53b20f92f2a64969a50c4
1/* 2 * Copyright (C) 2010 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 com.android.layoutlib.bridge.impl; 18 19import static com.android.ide.common.rendering.api.Result.Status.ERROR_ANIM_NOT_FOUND; 20import static com.android.ide.common.rendering.api.Result.Status.ERROR_INFLATION; 21import static com.android.ide.common.rendering.api.Result.Status.ERROR_LOCK_INTERRUPTED; 22import static com.android.ide.common.rendering.api.Result.Status.ERROR_NOT_INFLATED; 23import static com.android.ide.common.rendering.api.Result.Status.ERROR_TIMEOUT; 24import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN; 25import static com.android.ide.common.rendering.api.Result.Status.ERROR_VIEWGROUP_NO_CHILDREN; 26import static com.android.ide.common.rendering.api.Result.Status.SUCCESS; 27 28import com.android.ide.common.rendering.api.IAnimationListener; 29import com.android.ide.common.rendering.api.ILayoutPullParser; 30import com.android.ide.common.rendering.api.IProjectCallback; 31import com.android.ide.common.rendering.api.LayoutLog; 32import com.android.ide.common.rendering.api.Params; 33import com.android.ide.common.rendering.api.RenderResources; 34import com.android.ide.common.rendering.api.RenderSession; 35import com.android.ide.common.rendering.api.ResourceDensity; 36import com.android.ide.common.rendering.api.ResourceValue; 37import com.android.ide.common.rendering.api.Result; 38import com.android.ide.common.rendering.api.StyleResourceValue; 39import com.android.ide.common.rendering.api.ViewInfo; 40import com.android.ide.common.rendering.api.Params.RenderingMode; 41import com.android.ide.common.rendering.api.RenderResources.FrameworkResourceIdProvider; 42import com.android.ide.common.rendering.api.Result.Status; 43import com.android.internal.util.XmlUtils; 44import com.android.layoutlib.bridge.Bridge; 45import com.android.layoutlib.bridge.android.BridgeContext; 46import com.android.layoutlib.bridge.android.BridgeInflater; 47import com.android.layoutlib.bridge.android.BridgeLayoutParamsMapAttributes; 48import com.android.layoutlib.bridge.android.BridgeWindow; 49import com.android.layoutlib.bridge.android.BridgeWindowSession; 50import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; 51 52import android.animation.Animator; 53import android.animation.AnimatorInflater; 54import android.animation.LayoutTransition; 55import android.animation.LayoutTransition.TransitionListener; 56import android.app.Fragment_Delegate; 57import android.graphics.Bitmap; 58import android.graphics.Bitmap_Delegate; 59import android.graphics.Canvas; 60import android.graphics.drawable.Drawable; 61import android.os.Handler; 62import android.util.DisplayMetrics; 63import android.util.TypedValue; 64import android.view.View; 65import android.view.ViewGroup; 66import android.view.View.AttachInfo; 67import android.view.View.MeasureSpec; 68import android.view.ViewGroup.LayoutParams; 69import android.widget.FrameLayout; 70import android.widget.TabHost; 71import android.widget.TabWidget; 72 73import java.awt.Color; 74import java.awt.Graphics2D; 75import java.awt.image.BufferedImage; 76import java.util.ArrayList; 77import java.util.List; 78import java.util.Map; 79import java.util.concurrent.TimeUnit; 80import java.util.concurrent.locks.ReentrantLock; 81 82/** 83 * Class implementing the render session. 84 * 85 * A session is a stateful representation of a layout file. It is initialized with data coming 86 * through the {@link Bridge} API to inflate the layout. Further actions and rendering can then 87 * be done on the layout. 88 * 89 */ 90public class RenderSessionImpl extends FrameworkResourceIdProvider { 91 92 private static final int DEFAULT_TITLE_BAR_HEIGHT = 25; 93 private static final int DEFAULT_STATUS_BAR_HEIGHT = 25; 94 95 /** 96 * The current context being rendered. This is set through {@link #acquire(long)} and 97 * {@link #init(long)}, and unset in {@link #release()}. 98 */ 99 private static BridgeContext sCurrentContext = null; 100 101 private final Params mParams; 102 103 // scene state 104 private RenderSession mScene; 105 private BridgeContext mContext; 106 private BridgeXmlBlockParser mBlockParser; 107 private BridgeInflater mInflater; 108 private int mScreenOffset; 109 private ResourceValue mWindowBackground; 110 private FrameLayout mViewRoot; 111 private Canvas mCanvas; 112 private int mMeasuredScreenWidth = -1; 113 private int mMeasuredScreenHeight = -1; 114 115 // information being returned through the API 116 private BufferedImage mImage; 117 private ViewInfo mViewInfo; 118 119 private static final class PostInflateException extends Exception { 120 private static final long serialVersionUID = 1L; 121 122 public PostInflateException(String message) { 123 super(message); 124 } 125 } 126 127 /** 128 * Creates a layout scene with all the information coming from the layout bridge API. 129 * <p> 130 * This <b>must</b> be followed by a call to {@link RenderSessionImpl#init()}, which act as a 131 * call to {@link RenderSessionImpl#acquire(long)} 132 * 133 * @see LayoutBridge#createScene(com.android.layoutlib.api.SceneParams) 134 */ 135 public RenderSessionImpl(Params params) { 136 // copy the params. 137 mParams = new Params(params); 138 } 139 140 /** 141 * Initializes and acquires the scene, creating various Android objects such as context, 142 * inflater, and parser. 143 * 144 * @param timeout the time to wait if another rendering is happening. 145 * 146 * @return whether the scene was prepared 147 * 148 * @see #acquire(long) 149 * @see #release() 150 */ 151 public Result init(long timeout) { 152 // acquire the lock. if the result is null, lock was just acquired, otherwise, return 153 // the result. 154 Result result = acquireLock(timeout); 155 if (result != null) { 156 return result; 157 } 158 159 // setup the display Metrics. 160 DisplayMetrics metrics = new DisplayMetrics(); 161 metrics.densityDpi = mParams.getDensity(); 162 metrics.density = mParams.getDensity() / (float) DisplayMetrics.DENSITY_DEFAULT; 163 metrics.scaledDensity = metrics.density; 164 metrics.widthPixels = mParams.getScreenWidth(); 165 metrics.heightPixels = mParams.getScreenHeight(); 166 metrics.xdpi = mParams.getXdpi(); 167 metrics.ydpi = mParams.getYdpi(); 168 169 RenderResources resources = mParams.getResources(); 170 171 // build the context 172 mContext = new BridgeContext(mParams.getProjectKey(), metrics, resources, 173 mParams.getProjectCallback()); 174 175 176 setUp(); 177 178 // get the screen offset and window-background resource 179 mWindowBackground = null; 180 mScreenOffset = 0; 181 StyleResourceValue theme = resources.getTheme(); 182 if (theme != null && mParams.isBgColorOverridden() == false) { 183 mWindowBackground = resources.findItemInTheme("windowBackground"); 184 mWindowBackground = resources.resolveResValue(mWindowBackground); 185 186 mScreenOffset = getScreenOffset(resources, metrics); 187 } 188 189 // build the inflater and parser. 190 mInflater = new BridgeInflater(mContext, mParams.getProjectCallback()); 191 mContext.setBridgeInflater(mInflater); 192 mInflater.setFactory2(mContext); 193 194 mBlockParser = new BridgeXmlBlockParser(mParams.getLayoutDescription(), 195 mContext, false /* platformResourceFlag */); 196 197 return SUCCESS.createResult(); 198 } 199 200 /** 201 * Prepares the scene for action. 202 * <p> 203 * This call is blocking if another rendering/inflating is currently happening, and will return 204 * whether the preparation worked. 205 * 206 * The preparation can fail if another rendering took too long and the timeout was elapsed. 207 * 208 * More than one call to this from the same thread will have no effect and will return 209 * {@link Result#SUCCESS}. 210 * 211 * After scene actions have taken place, only one call to {@link #release()} must be 212 * done. 213 * 214 * @param timeout the time to wait if another rendering is happening. 215 * 216 * @return whether the scene was prepared 217 * 218 * @see #release() 219 * 220 * @throws IllegalStateException if {@link #init(long)} was never called. 221 */ 222 public Result acquire(long timeout) { 223 if (mContext == null) { 224 throw new IllegalStateException("After scene creation, #init() must be called"); 225 } 226 227 // acquire the lock. if the result is null, lock was just acquired, otherwise, return 228 // the result. 229 Result result = acquireLock(timeout); 230 if (result != null) { 231 return result; 232 } 233 234 setUp(); 235 236 return SUCCESS.createResult(); 237 } 238 239 /** 240 * Acquire the lock so that the scene can be acted upon. 241 * <p> 242 * This returns null if the lock was just acquired, otherwise it returns 243 * {@link Result#SUCCESS} if the lock already belonged to that thread, or another 244 * instance (see {@link Result#getStatus()}) if an error occurred. 245 * 246 * @param timeout the time to wait if another rendering is happening. 247 * @return null if the lock was just acquire or another result depending on the state. 248 * 249 * @throws IllegalStateException if the current context is different than the one owned by 250 * the scene. 251 */ 252 private Result acquireLock(long timeout) { 253 ReentrantLock lock = Bridge.getLock(); 254 if (lock.isHeldByCurrentThread() == false) { 255 try { 256 boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS); 257 258 if (acquired == false) { 259 return ERROR_TIMEOUT.createResult(); 260 } 261 } catch (InterruptedException e) { 262 return ERROR_LOCK_INTERRUPTED.createResult(); 263 } 264 } else { 265 // This thread holds the lock already. Checks that this wasn't for a different context. 266 // If this is called by init, mContext will be null and so should sCurrentContext 267 // anyway 268 if (mContext != sCurrentContext) { 269 throw new IllegalStateException("Acquiring different scenes from same thread without releases"); 270 } 271 return SUCCESS.createResult(); 272 } 273 274 return null; 275 } 276 277 /** 278 * Cleans up the scene after an action. 279 */ 280 public void release() { 281 ReentrantLock lock = Bridge.getLock(); 282 283 // with the use of finally blocks, it is possible to find ourself calling this 284 // without a successful call to prepareScene. This test makes sure that unlock() will 285 // not throw IllegalMonitorStateException. 286 if (lock.isHeldByCurrentThread()) { 287 tearDown(); 288 lock.unlock(); 289 } 290 } 291 292 /** 293 * Sets up the session for rendering. 294 * <p/> 295 * The counterpart is {@link #tearDown()}. 296 */ 297 private void setUp() { 298 // make sure the Resources object references the context (and other objects) for this 299 // scene 300 mContext.initResources(); 301 sCurrentContext = mContext; 302 303 LayoutLog currentLog = mParams.getLog(); 304 Bridge.setLog(currentLog); 305 mContext.getRenderResources().setFrameworkResourceIdProvider(this); 306 mContext.getRenderResources().setLogger(currentLog); 307 } 308 309 /** 310 * Tear down the session after rendering. 311 * <p/> 312 * The counterpart is {@link #setUp()}. 313 */ 314 private void tearDown() { 315 // Make sure to remove static references, otherwise we could not unload the lib 316 mContext.disposeResources(); 317 sCurrentContext = null; 318 319 Bridge.setLog(null); 320 mContext.getRenderResources().setFrameworkResourceIdProvider(null); 321 mContext.getRenderResources().setLogger(null); 322 323 } 324 325 /** 326 * Inflates the layout. 327 * <p> 328 * {@link #acquire(long)} must have been called before this. 329 * 330 * @throws IllegalStateException if the current context is different than the one owned by 331 * the scene, or if {@link #init(long)} was not called. 332 */ 333 public Result inflate() { 334 checkLock(); 335 336 try { 337 338 mViewRoot = new FrameLayout(mContext); 339 340 // Sets the project callback (custom view loader) to the fragment delegate so that 341 // it can instantiate the custom Fragment. 342 Fragment_Delegate.setProjectCallback(mParams.getProjectCallback()); 343 344 View view = mInflater.inflate(mBlockParser, mViewRoot); 345 346 // post-inflate process. For now this supports TabHost/TabWidget 347 postInflateProcess(view, mParams.getProjectCallback()); 348 349 Fragment_Delegate.setProjectCallback(null); 350 351 // set the AttachInfo on the root view. 352 AttachInfo info = new AttachInfo(new BridgeWindowSession(), new BridgeWindow(), 353 new Handler(), null); 354 info.mHasWindowFocus = true; 355 info.mWindowVisibility = View.VISIBLE; 356 info.mInTouchMode = false; // this is so that we can display selections. 357 info.mHardwareAccelerated = false; 358 mViewRoot.dispatchAttachedToWindow(info, 0); 359 360 // get the background drawable 361 if (mWindowBackground != null) { 362 Drawable d = ResourceHelper.getDrawable(mWindowBackground, 363 mContext, true /* isFramework */); 364 mViewRoot.setBackgroundDrawable(d); 365 } 366 367 return SUCCESS.createResult(); 368 } catch (PostInflateException e) { 369 return ERROR_INFLATION.createResult(e.getMessage(), e); 370 } catch (Throwable e) { 371 // get the real cause of the exception. 372 Throwable t = e; 373 while (t.getCause() != null) { 374 t = t.getCause(); 375 } 376 377 return ERROR_INFLATION.createResult(t.getMessage(), t); 378 } 379 } 380 381 /** 382 * Renders the scene. 383 * <p> 384 * {@link #acquire(long)} must have been called before this. 385 * 386 * @throws IllegalStateException if the current context is different than the one owned by 387 * the scene, or if {@link #acquire(long)} was not called. 388 * 389 * @see SceneParams#getRenderingMode() 390 * @see LayoutScene#render(long) 391 */ 392 public Result render() { 393 checkLock(); 394 395 try { 396 if (mViewRoot == null) { 397 return ERROR_NOT_INFLATED.createResult(); 398 } 399 // measure the views 400 int w_spec, h_spec; 401 402 RenderingMode renderingMode = mParams.getRenderingMode(); 403 404 // only do the screen measure when needed. 405 boolean newRenderSize = false; 406 if (mMeasuredScreenWidth == -1) { 407 newRenderSize = true; 408 mMeasuredScreenWidth = mParams.getScreenWidth(); 409 mMeasuredScreenHeight = mParams.getScreenHeight(); 410 411 if (renderingMode != RenderingMode.NORMAL) { 412 // measure the full size needed by the layout. 413 w_spec = MeasureSpec.makeMeasureSpec(mMeasuredScreenWidth, 414 renderingMode.isHorizExpand() ? 415 MeasureSpec.UNSPECIFIED // this lets us know the actual needed size 416 : MeasureSpec.EXACTLY); 417 h_spec = MeasureSpec.makeMeasureSpec(mMeasuredScreenHeight - mScreenOffset, 418 renderingMode.isVertExpand() ? 419 MeasureSpec.UNSPECIFIED // this lets us know the actual needed size 420 : MeasureSpec.EXACTLY); 421 mViewRoot.measure(w_spec, h_spec); 422 423 if (renderingMode.isHorizExpand()) { 424 int neededWidth = mViewRoot.getChildAt(0).getMeasuredWidth(); 425 if (neededWidth > mMeasuredScreenWidth) { 426 mMeasuredScreenWidth = neededWidth; 427 } 428 } 429 430 if (renderingMode.isVertExpand()) { 431 int neededHeight = mViewRoot.getChildAt(0).getMeasuredHeight(); 432 if (neededHeight > mMeasuredScreenHeight - mScreenOffset) { 433 mMeasuredScreenHeight = neededHeight + mScreenOffset; 434 } 435 } 436 } 437 } 438 439 // remeasure with the size we need 440 // This must always be done before the call to layout 441 w_spec = MeasureSpec.makeMeasureSpec(mMeasuredScreenWidth, MeasureSpec.EXACTLY); 442 h_spec = MeasureSpec.makeMeasureSpec(mMeasuredScreenHeight - mScreenOffset, 443 MeasureSpec.EXACTLY); 444 mViewRoot.measure(w_spec, h_spec); 445 446 // now do the layout. 447 mViewRoot.layout(0, mScreenOffset, mMeasuredScreenWidth, mMeasuredScreenHeight); 448 449 // draw the views 450 // create the BufferedImage into which the layout will be rendered. 451 if (newRenderSize || mCanvas == null) { 452 if (mParams.getImageFactory() != null) { 453 mImage = mParams.getImageFactory().getImage(mMeasuredScreenWidth, 454 mMeasuredScreenHeight - mScreenOffset); 455 } else { 456 mImage = new BufferedImage(mMeasuredScreenWidth, 457 mMeasuredScreenHeight - mScreenOffset, BufferedImage.TYPE_INT_ARGB); 458 } 459 460 if (mParams.isBgColorOverridden()) { 461 Graphics2D gc = mImage.createGraphics(); 462 gc.setColor(new Color(mParams.getOverrideBgColor(), true)); 463 gc.fillRect(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight - mScreenOffset); 464 gc.dispose(); 465 } 466 467 // create an Android bitmap around the BufferedImage 468 Bitmap bitmap = Bitmap_Delegate.createBitmap(mImage, 469 true /*isMutable*/, 470 ResourceDensity.getEnum(mParams.getDensity())); 471 472 // create a Canvas around the Android bitmap 473 mCanvas = new Canvas(bitmap); 474 mCanvas.setDensity(mParams.getDensity()); 475 } 476 477 mViewRoot.draw(mCanvas); 478 479 mViewInfo = visit(((ViewGroup)mViewRoot).getChildAt(0), mContext); 480 481 // success! 482 return SUCCESS.createResult(); 483 } catch (Throwable e) { 484 // get the real cause of the exception. 485 Throwable t = e; 486 while (t.getCause() != null) { 487 t = t.getCause(); 488 } 489 490 return ERROR_UNKNOWN.createResult(t.getMessage(), t); 491 } 492 } 493 494 /** 495 * Animate an object 496 * <p> 497 * {@link #acquire(long)} must have been called before this. 498 * 499 * @throws IllegalStateException if the current context is different than the one owned by 500 * the scene, or if {@link #acquire(long)} was not called. 501 * 502 * @see LayoutScene#animate(Object, String, boolean, IAnimationListener) 503 */ 504 public Result animate(Object targetObject, String animationName, 505 boolean isFrameworkAnimation, IAnimationListener listener) { 506 checkLock(); 507 508 // find the animation file. 509 ResourceValue animationResource = null; 510 int animationId = 0; 511 if (isFrameworkAnimation) { 512 animationResource = mContext.getRenderResources().getFrameworkResource( 513 RenderResources.RES_ANIMATOR, animationName); 514 if (animationResource != null) { 515 animationId = Bridge.getResourceValue(RenderResources.RES_ANIMATOR, 516 animationName); 517 } 518 } else { 519 animationResource = mContext.getRenderResources().getProjectResource( 520 RenderResources.RES_ANIMATOR, animationName); 521 if (animationResource != null) { 522 animationId = mContext.getProjectCallback().getResourceValue( 523 RenderResources.RES_ANIMATOR, animationName); 524 } 525 } 526 527 if (animationResource != null) { 528 try { 529 Animator anim = AnimatorInflater.loadAnimator(mContext, animationId); 530 if (anim != null) { 531 anim.setTarget(targetObject); 532 533 new PlayAnimationThread(anim, this, animationName, listener).start(); 534 535 return SUCCESS.createResult(); 536 } 537 } catch (Exception e) { 538 // get the real cause of the exception. 539 Throwable t = e; 540 while (t.getCause() != null) { 541 t = t.getCause(); 542 } 543 544 return ERROR_UNKNOWN.createResult(t.getMessage(), t); 545 } 546 } 547 548 return ERROR_ANIM_NOT_FOUND.createResult(); 549 } 550 551 /** 552 * Insert a new child into an existing parent. 553 * <p> 554 * {@link #acquire(long)} must have been called before this. 555 * 556 * @throws IllegalStateException if the current context is different than the one owned by 557 * the scene, or if {@link #acquire(long)} was not called. 558 * 559 * @see LayoutScene#insertChild(Object, ILayoutPullParser, int, IAnimationListener) 560 */ 561 public Result insertChild(final ViewGroup parentView, ILayoutPullParser childXml, 562 final int index, IAnimationListener listener) { 563 checkLock(); 564 565 // create a block parser for the XML 566 BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser(childXml, mContext, 567 false /* platformResourceFlag */); 568 569 // inflate the child without adding it to the root since we want to control where it'll 570 // get added. We do pass the parentView however to ensure that the layoutParams will 571 // be created correctly. 572 final View child = mInflater.inflate(blockParser, parentView, false /*attachToRoot*/); 573 574 invalidateRenderingSize(); 575 576 if (listener != null) { 577 new AnimationThread(this, "insertChild", listener) { 578 579 @Override 580 public Result preAnimation() { 581 parentView.setLayoutTransition(new LayoutTransition()); 582 return addView(parentView, child, index); 583 } 584 585 @Override 586 public void postAnimation() { 587 parentView.setLayoutTransition(null); 588 } 589 }.start(); 590 591 // always return success since the real status will come through the listener. 592 return SUCCESS.createResult(child); 593 } 594 595 // add it to the parentView in the correct location 596 Result result = addView(parentView, child, index); 597 if (result.isSuccess() == false) { 598 return result; 599 } 600 601 result = render(); 602 if (result.isSuccess()) { 603 result = result.getCopyWithData(child); 604 } 605 606 return result; 607 } 608 609 /** 610 * Adds a given view to a given parent at a given index. 611 * 612 * @param parent the parent to receive the view 613 * @param view the view to add to the parent 614 * @param index the index where to do the add. 615 * 616 * @return a Result with {@link Status#SUCCESS} or 617 * {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support 618 * adding views. 619 */ 620 private Result addView(ViewGroup parent, View view, int index) { 621 try { 622 parent.addView(view, index); 623 return SUCCESS.createResult(); 624 } catch (UnsupportedOperationException e) { 625 // looks like this is a view class that doesn't support children manipulation! 626 return ERROR_VIEWGROUP_NO_CHILDREN.createResult(); 627 } 628 } 629 630 /** 631 * Moves a view to a new parent at a given location 632 * <p> 633 * {@link #acquire(long)} must have been called before this. 634 * 635 * @throws IllegalStateException if the current context is different than the one owned by 636 * the scene, or if {@link #acquire(long)} was not called. 637 * 638 * @see LayoutScene#moveChild(Object, Object, int, Map, IAnimationListener) 639 */ 640 public Result moveChild(final ViewGroup newParentView, final View childView, final int index, 641 Map<String, String> layoutParamsMap, final IAnimationListener listener) { 642 checkLock(); 643 644 invalidateRenderingSize(); 645 646 LayoutParams layoutParams = null; 647 if (layoutParamsMap != null) { 648 // need to create a new LayoutParams object for the new parent. 649 layoutParams = newParentView.generateLayoutParams( 650 new BridgeLayoutParamsMapAttributes(layoutParamsMap)); 651 } 652 653 // get the current parent of the view that needs to be moved. 654 final ViewGroup previousParent = (ViewGroup) childView.getParent(); 655 656 if (listener != null) { 657 final LayoutParams params = layoutParams; 658 659 // there is no support for animating views across layouts, so in case the new and old 660 // parent views are different we fake the animation through a no animation thread. 661 if (previousParent != newParentView) { 662 new Thread("not animated moveChild") { 663 @Override 664 public void run() { 665 Result result = moveView(previousParent, newParentView, childView, index, 666 params); 667 if (result.isSuccess() == false) { 668 listener.done(result); 669 } 670 671 // ready to do the work, acquire the scene. 672 result = acquire(250); 673 if (result.isSuccess() == false) { 674 listener.done(result); 675 return; 676 } 677 678 try { 679 result = render(); 680 if (result.isSuccess()) { 681 listener.onNewFrame(RenderSessionImpl.this.getSession()); 682 } 683 } finally { 684 release(); 685 } 686 687 listener.done(result); 688 } 689 }.start(); 690 } else { 691 new AnimationThread(this, "moveChild", listener) { 692 693 @Override 694 public Result preAnimation() { 695 // set up the transition for the parent. 696 LayoutTransition transition = new LayoutTransition(); 697 previousParent.setLayoutTransition(transition); 698 699 // tweak the animation durations and start delays (to match the duration of 700 // animation playing just before). 701 // Note: Cannot user Animation.setDuration() directly. Have to set it 702 // on the LayoutTransition. 703 transition.setDuration(LayoutTransition.DISAPPEARING, 100); 704 // CHANGE_DISAPPEARING plays after DISAPPEARING 705 transition.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 100); 706 707 transition.setDuration(LayoutTransition.CHANGE_DISAPPEARING, 100); 708 709 transition.setDuration(LayoutTransition.CHANGE_APPEARING, 100); 710 // CHANGE_APPEARING plays after CHANGE_APPEARING 711 transition.setStartDelay(LayoutTransition.APPEARING, 100); 712 713 transition.setDuration(LayoutTransition.APPEARING, 100); 714 715 return moveView(previousParent, newParentView, childView, index, params); 716 } 717 718 @Override 719 public void postAnimation() { 720 previousParent.setLayoutTransition(null); 721 newParentView.setLayoutTransition(null); 722 } 723 }.start(); 724 } 725 726 // always return success since the real status will come through the listener. 727 return SUCCESS.createResult(layoutParams); 728 } 729 730 Result result = moveView(previousParent, newParentView, childView, index, layoutParams); 731 if (result.isSuccess() == false) { 732 return result; 733 } 734 735 result = render(); 736 if (layoutParams != null && result.isSuccess()) { 737 result = result.getCopyWithData(layoutParams); 738 } 739 740 return result; 741 } 742 743 /** 744 * Moves a View from its current parent to a new given parent at a new given location, with 745 * an optional new {@link LayoutParams} instance 746 * 747 * @param previousParent the previous parent, still owning the child at the time of the call. 748 * @param newParent the new parent 749 * @param movedView the view to move 750 * @param index the new location in the new parent 751 * @param params an option (can be null) {@link LayoutParams} instance. 752 * 753 * @return a Result with {@link Status#SUCCESS} or 754 * {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support 755 * adding views. 756 */ 757 private Result moveView(ViewGroup previousParent, final ViewGroup newParent, 758 final View movedView, final int index, final LayoutParams params) { 759 try { 760 // check if there is a transition on the previousParent. 761 LayoutTransition previousTransition = previousParent.getLayoutTransition(); 762 if (previousTransition != null) { 763 // in this case there is an animation. This means we have to wait for the child's 764 // parent reference to be null'ed out so that we can add it to the new parent. 765 // It is technically removed right before the DISAPPEARING animation is done (if 766 // the animation of this type is not null, otherwise it's after which is impossible 767 // to handle). 768 // Because there is no move animation, if the new parent is the same as the old 769 // parent, we need to wait until the CHANGE_DISAPPEARING animation is done before 770 // adding the child or the child will appear in its new location before the 771 // other children have made room for it. 772 773 // add a listener to the transition to be notified of the actual removal. 774 previousTransition.addTransitionListener(new TransitionListener() { 775 private int mChangeDisappearingCount = 0; 776 777 public void startTransition(LayoutTransition transition, ViewGroup container, 778 View view, int transitionType) { 779 if (transitionType == LayoutTransition.CHANGE_DISAPPEARING) { 780 mChangeDisappearingCount++; 781 } 782 } 783 784 public void endTransition(LayoutTransition transition, ViewGroup container, 785 View view, int transitionType) { 786 if (transitionType == LayoutTransition.CHANGE_DISAPPEARING) { 787 mChangeDisappearingCount--; 788 } 789 790 if (transitionType == LayoutTransition.CHANGE_DISAPPEARING && 791 mChangeDisappearingCount == 0) { 792 // add it to the parentView in the correct location 793 if (params != null) { 794 newParent.addView(movedView, index, params); 795 } else { 796 newParent.addView(movedView, index); 797 } 798 } 799 } 800 }); 801 802 // remove the view from the current parent. 803 previousParent.removeView(movedView); 804 805 // and return since adding the view to the new parent is done in the listener. 806 return SUCCESS.createResult(); 807 } else { 808 // standard code with no animation. pretty simple. 809 previousParent.removeView(movedView); 810 811 // add it to the parentView in the correct location 812 if (params != null) { 813 newParent.addView(movedView, index, params); 814 } else { 815 newParent.addView(movedView, index); 816 } 817 818 return SUCCESS.createResult(); 819 } 820 } catch (UnsupportedOperationException e) { 821 // looks like this is a view class that doesn't support children manipulation! 822 return ERROR_VIEWGROUP_NO_CHILDREN.createResult(); 823 } 824 } 825 826 /** 827 * Removes a child from its current parent. 828 * <p> 829 * {@link #acquire(long)} must have been called before this. 830 * 831 * @throws IllegalStateException if the current context is different than the one owned by 832 * the scene, or if {@link #acquire(long)} was not called. 833 * 834 * @see LayoutScene#removeChild(Object, IAnimationListener) 835 */ 836 public Result removeChild(final View childView, IAnimationListener listener) { 837 checkLock(); 838 839 invalidateRenderingSize(); 840 841 final ViewGroup parent = (ViewGroup) childView.getParent(); 842 843 if (listener != null) { 844 new AnimationThread(this, "moveChild", listener) { 845 846 @Override 847 public Result preAnimation() { 848 parent.setLayoutTransition(new LayoutTransition()); 849 return removeView(parent, childView); 850 } 851 852 @Override 853 public void postAnimation() { 854 parent.setLayoutTransition(null); 855 } 856 }.start(); 857 858 // always return success since the real status will come through the listener. 859 return SUCCESS.createResult(); 860 } 861 862 Result result = removeView(parent, childView); 863 if (result.isSuccess() == false) { 864 return result; 865 } 866 867 return render(); 868 } 869 870 /** 871 * Removes a given view from its current parent. 872 * 873 * @param view the view to remove from its parent 874 * 875 * @return a Result with {@link Status#SUCCESS} or 876 * {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support 877 * adding views. 878 */ 879 private Result removeView(ViewGroup parent, View view) { 880 try { 881 parent.removeView(view); 882 return SUCCESS.createResult(); 883 } catch (UnsupportedOperationException e) { 884 // looks like this is a view class that doesn't support children manipulation! 885 return ERROR_VIEWGROUP_NO_CHILDREN.createResult(); 886 } 887 } 888 889 /** 890 * Returns the log associated with the session. 891 * @return the log or null if there are none. 892 */ 893 public LayoutLog getLog() { 894 if (mParams != null) { 895 return mParams.getLog(); 896 } 897 898 return null; 899 } 900 901 /** 902 * Checks that the lock is owned by the current thread and that the current context is the one 903 * from this scene. 904 * 905 * @throws IllegalStateException if the current context is different than the one owned by 906 * the scene, or if {@link #acquire(long)} was not called. 907 */ 908 private void checkLock() { 909 ReentrantLock lock = Bridge.getLock(); 910 if (lock.isHeldByCurrentThread() == false) { 911 throw new IllegalStateException("scene must be acquired first. see #acquire(long)"); 912 } 913 if (sCurrentContext != mContext) { 914 throw new IllegalStateException("Thread acquired a scene but is rendering a different one"); 915 } 916 } 917 918 /** 919 * Returns the top screen offset. This depends on whether the current theme defines the user 920 * of the title and status bars. 921 * @param resolver The {@link ResourceResolver} 922 * @param metrics The display metrics 923 * @return the pixel height offset 924 */ 925 private int getScreenOffset(RenderResources resolver, DisplayMetrics metrics) { 926 int offset = 0; 927 928 // get the title bar flag from the current theme. 929 ResourceValue value = resolver.findItemInTheme("windowNoTitle"); 930 931 // because it may reference something else, we resolve it. 932 value = resolver.resolveResValue(value); 933 934 // if there's a value and it's true (default is false) 935 if (value == null || value.getValue() == null || 936 XmlUtils.convertValueToBoolean(value.getValue(), false /* defValue */) == false) { 937 // default size of the window title bar 938 int defaultOffset = DEFAULT_TITLE_BAR_HEIGHT; 939 940 // get value from the theme. 941 value = resolver.findItemInTheme("windowTitleSize"); 942 943 // resolve it 944 value = resolver.resolveResValue(value); 945 946 if (value != null) { 947 // get the numerical value, if available 948 TypedValue typedValue = ResourceHelper.getValue(value.getValue()); 949 if (typedValue != null) { 950 // compute the pixel value based on the display metrics 951 defaultOffset = (int)typedValue.getDimension(metrics); 952 } 953 } 954 955 offset += defaultOffset; 956 } 957 958 // get the fullscreen flag from the current theme. 959 value = resolver.findItemInTheme("windowFullscreen"); 960 961 // because it may reference something else, we resolve it. 962 value = resolver.resolveResValue(value); 963 964 if (value == null || value.getValue() == null || 965 XmlUtils.convertValueToBoolean(value.getValue(), false /* defValue */) == false) { 966 967 // default value 968 int defaultOffset = DEFAULT_STATUS_BAR_HEIGHT; 969 970 // get the real value 971 value = resolver.getFrameworkResource(RenderResources.RES_DIMEN, "status_bar_height"); 972 if (value != null) { 973 TypedValue typedValue = ResourceHelper.getValue(value.getValue()); 974 if (typedValue != null) { 975 // compute the pixel value based on the display metrics 976 defaultOffset = (int)typedValue.getDimension(metrics); 977 } 978 } 979 980 // add the computed offset. 981 offset += defaultOffset; 982 } 983 984 return offset; 985 } 986 987 /** 988 * Post process on a view hierachy that was just inflated. 989 * <p/>At the moment this only support TabHost: If {@link TabHost} is detected, look for the 990 * {@link TabWidget}, and the corresponding {@link FrameLayout} and make new tabs automatically 991 * based on the content of the {@link FrameLayout}. 992 * @param view the root view to process. 993 * @param projectCallback callback to the project. 994 */ 995 private void postInflateProcess(View view, IProjectCallback projectCallback) 996 throws PostInflateException { 997 if (view instanceof TabHost) { 998 setupTabHost((TabHost)view, projectCallback); 999 } else if (view instanceof ViewGroup) { 1000 ViewGroup group = (ViewGroup)view; 1001 final int count = group.getChildCount(); 1002 for (int c = 0 ; c < count ; c++) { 1003 View child = group.getChildAt(c); 1004 postInflateProcess(child, projectCallback); 1005 } 1006 } 1007 } 1008 1009 /** 1010 * Sets up a {@link TabHost} object. 1011 * @param tabHost the TabHost to setup. 1012 * @param projectCallback The project callback object to access the project R class. 1013 * @throws PostInflateException 1014 */ 1015 private void setupTabHost(TabHost tabHost, IProjectCallback projectCallback) 1016 throws PostInflateException { 1017 // look for the TabWidget, and the FrameLayout. They have their own specific names 1018 View v = tabHost.findViewById(android.R.id.tabs); 1019 1020 if (v == null) { 1021 throw new PostInflateException( 1022 "TabHost requires a TabWidget with id \"android:id/tabs\".\n"); 1023 } 1024 1025 if ((v instanceof TabWidget) == false) { 1026 throw new PostInflateException(String.format( 1027 "TabHost requires a TabWidget with id \"android:id/tabs\".\n" + 1028 "View found with id 'tabs' is '%s'", v.getClass().getCanonicalName())); 1029 } 1030 1031 v = tabHost.findViewById(android.R.id.tabcontent); 1032 1033 if (v == null) { 1034 // TODO: see if we can fake tabs even without the FrameLayout (same below when the framelayout is empty) 1035 throw new PostInflateException( 1036 "TabHost requires a FrameLayout with id \"android:id/tabcontent\"."); 1037 } 1038 1039 if ((v instanceof FrameLayout) == false) { 1040 throw new PostInflateException(String.format( 1041 "TabHost requires a FrameLayout with id \"android:id/tabcontent\".\n" + 1042 "View found with id 'tabcontent' is '%s'", v.getClass().getCanonicalName())); 1043 } 1044 1045 FrameLayout content = (FrameLayout)v; 1046 1047 // now process the content of the framelayout and dynamically create tabs for it. 1048 final int count = content.getChildCount(); 1049 1050 if (count == 0) { 1051 throw new PostInflateException( 1052 "The FrameLayout for the TabHost has no content. Rendering failed.\n"); 1053 } 1054 1055 // this must be called before addTab() so that the TabHost searches its TabWidget 1056 // and FrameLayout. 1057 tabHost.setup(); 1058 1059 // for each child of the framelayout, add a new TabSpec 1060 for (int i = 0 ; i < count ; i++) { 1061 View child = content.getChildAt(i); 1062 String tabSpec = String.format("tab_spec%d", i+1); 1063 int id = child.getId(); 1064 String[] resource = projectCallback.resolveResourceValue(id); 1065 String name; 1066 if (resource != null) { 1067 name = resource[0]; // 0 is resource name, 1 is resource type. 1068 } else { 1069 name = String.format("Tab %d", i+1); // default name if id is unresolved. 1070 } 1071 tabHost.addTab(tabHost.newTabSpec(tabSpec).setIndicator(name).setContent(id)); 1072 } 1073 } 1074 1075 1076 /** 1077 * Visits a View and its children and generate a {@link ViewInfo} containing the 1078 * bounds of all the views. 1079 * @param view the root View 1080 * @param context the context. 1081 */ 1082 private ViewInfo visit(View view, BridgeContext context) { 1083 if (view == null) { 1084 return null; 1085 } 1086 1087 ViewInfo result = new ViewInfo(view.getClass().getName(), 1088 context.getViewKey(view), 1089 view.getLeft(), view.getTop(), view.getRight(), view.getBottom(), 1090 view, view.getLayoutParams()); 1091 1092 if (view instanceof ViewGroup) { 1093 ViewGroup group = ((ViewGroup) view); 1094 List<ViewInfo> children = new ArrayList<ViewInfo>(); 1095 for (int i = 0; i < group.getChildCount(); i++) { 1096 children.add(visit(group.getChildAt(i), context)); 1097 } 1098 result.setChildren(children); 1099 } 1100 1101 return result; 1102 } 1103 1104 private void invalidateRenderingSize() { 1105 mMeasuredScreenWidth = mMeasuredScreenHeight = -1; 1106 } 1107 1108 public BufferedImage getImage() { 1109 return mImage; 1110 } 1111 1112 public ViewInfo getViewInfo() { 1113 return mViewInfo; 1114 } 1115 1116 public Map<String, String> getDefaultProperties(Object viewObject) { 1117 return mContext.getDefaultPropMap(viewObject); 1118 } 1119 1120 public void setScene(RenderSession session) { 1121 mScene = session; 1122 } 1123 1124 public RenderSession getSession() { 1125 return mScene; 1126 } 1127 1128 // --- FrameworkResourceIdProvider methods 1129 1130 @Override 1131 public Integer getId(String resType, String resName) { 1132 return Bridge.getResourceValue(resType, resName); 1133 } 1134} 1135