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