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