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