RenderSessionImpl.java revision 345f866bfd09476fd62aa10345a0670cc110b63c
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 // post-inflate process. For now this supports TabHost/TabWidget 365 postInflateProcess(view, mParams.getProjectCallback()); 366 367 Fragment_Delegate.setProjectCallback(null); 368 369 // set the AttachInfo on the root view. 370 AttachInfo info = new AttachInfo(new BridgeWindowSession(), new BridgeWindow(), 371 new Handler(), null); 372 info.mHasWindowFocus = true; 373 info.mWindowVisibility = View.VISIBLE; 374 info.mInTouchMode = false; // this is so that we can display selections. 375 info.mHardwareAccelerated = false; 376 mViewRoot.dispatchAttachedToWindow(info, 0); 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, 465 MeasureSpec.EXACTLY); 466 mViewRoot.measure(w_spec, h_spec); 467 468 // now do the layout. 469 mViewRoot.layout(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight); 470 471 // draw the views 472 // create the BufferedImage into which the layout will be rendered. 473 boolean newImage = false; 474 if (newRenderSize || mCanvas == null) { 475 if (mParams.getImageFactory() != null) { 476 mImage = mParams.getImageFactory().getImage( 477 mMeasuredScreenWidth, 478 mMeasuredScreenHeight + mTotalBarSize); 479 } else { 480 mImage = new BufferedImage( 481 mMeasuredScreenWidth, 482 mMeasuredScreenHeight + mTotalBarSize, 483 BufferedImage.TYPE_INT_ARGB); 484 newImage = true; 485 } 486 487 if (mParams.isBgColorOverridden()) { 488 // since we override the content, it's the same as if it was a new image. 489 newImage = true; 490 Graphics2D gc = mImage.createGraphics(); 491 gc.setColor(new Color(mParams.getOverrideBgColor(), true)); 492 gc.setComposite(AlphaComposite.Src); 493 gc.fillRect(0, 0, mMeasuredScreenWidth, 494 mMeasuredScreenHeight + mTotalBarSize); 495 gc.dispose(); 496 } 497 498 // create an Android bitmap around the BufferedImage 499 Bitmap bitmap = Bitmap_Delegate.createBitmap(mImage, 500 true /*isMutable*/, 501 Density.getEnum(mParams.getDensity())); 502 503 // create a Canvas around the Android bitmap 504 mCanvas = new Canvas(bitmap); 505 mCanvas.setDensity(mParams.getDensity()); 506 mCanvas.translate(0, mTopOffset); 507 } 508 509 if (freshRender && newImage == false) { 510 Graphics2D gc = mImage.createGraphics(); 511 gc.setComposite(AlphaComposite.Src); 512 513 if (mStatusBarSize > 0) { 514 gc.setColor(new Color(0xFF3C3C3C, true)); 515 gc.fillRect(0, 0, mMeasuredScreenWidth, mStatusBarSize); 516 } 517 518 if (mTopBarSize > 0) { 519 gc.setColor(new Color(0xFF7F7F7F, true)); 520 gc.fillRect(0, mStatusBarSize, mMeasuredScreenWidth, mTopOffset); 521 } 522 523 // erase the rest 524 gc.setColor(new Color(0x00000000, true)); 525 gc.fillRect(0, mTopOffset, 526 mMeasuredScreenWidth, mMeasuredScreenHeight + mTopOffset); 527 528 if (mSystemBarSize > 0) { 529 gc.setColor(new Color(0xFF3C3C3C, true)); 530 gc.fillRect(0, mMeasuredScreenHeight + mTopOffset, 531 mMeasuredScreenWidth, mMeasuredScreenHeight + mTotalBarSize); 532 } 533 534 // done 535 gc.dispose(); 536 } 537 538 mViewRoot.draw(mCanvas); 539 540 mViewInfoList = visitAllChildren((ViewGroup)mViewRoot, mContext, mTopOffset); 541 542 // success! 543 return SUCCESS.createResult(); 544 } catch (Throwable e) { 545 // get the real cause of the exception. 546 Throwable t = e; 547 while (t.getCause() != null) { 548 t = t.getCause(); 549 } 550 551 return ERROR_UNKNOWN.createResult(t.getMessage(), t); 552 } 553 } 554 555 /** 556 * Animate an object 557 * <p> 558 * {@link #acquire(long)} must have been called before this. 559 * 560 * @throws IllegalStateException if the current context is different than the one owned by 561 * the scene, or if {@link #acquire(long)} was not called. 562 * 563 * @see LayoutScene#animate(Object, String, boolean, IAnimationListener) 564 */ 565 public Result animate(Object targetObject, String animationName, 566 boolean isFrameworkAnimation, IAnimationListener listener) { 567 checkLock(); 568 569 // find the animation file. 570 ResourceValue animationResource = null; 571 int animationId = 0; 572 if (isFrameworkAnimation) { 573 animationResource = mContext.getRenderResources().getFrameworkResource( 574 ResourceType.ANIMATOR, animationName); 575 if (animationResource != null) { 576 animationId = Bridge.getResourceId(ResourceType.ANIMATOR, animationName); 577 } 578 } else { 579 animationResource = mContext.getRenderResources().getProjectResource( 580 ResourceType.ANIMATOR, animationName); 581 if (animationResource != null) { 582 animationId = mContext.getProjectCallback().getResourceId( 583 ResourceType.ANIMATOR, animationName); 584 } 585 } 586 587 if (animationResource != null) { 588 try { 589 Animator anim = AnimatorInflater.loadAnimator(mContext, animationId); 590 if (anim != null) { 591 anim.setTarget(targetObject); 592 593 new PlayAnimationThread(anim, this, animationName, listener).start(); 594 595 return SUCCESS.createResult(); 596 } 597 } catch (Exception e) { 598 // get the real cause of the exception. 599 Throwable t = e; 600 while (t.getCause() != null) { 601 t = t.getCause(); 602 } 603 604 return ERROR_UNKNOWN.createResult(t.getMessage(), t); 605 } 606 } 607 608 return ERROR_ANIM_NOT_FOUND.createResult(); 609 } 610 611 /** 612 * Insert a new child into an existing parent. 613 * <p> 614 * {@link #acquire(long)} must have been called before this. 615 * 616 * @throws IllegalStateException if the current context is different than the one owned by 617 * the scene, or if {@link #acquire(long)} was not called. 618 * 619 * @see LayoutScene#insertChild(Object, ILayoutPullParser, int, IAnimationListener) 620 */ 621 public Result insertChild(final ViewGroup parentView, ILayoutPullParser childXml, 622 final int index, IAnimationListener listener) { 623 checkLock(); 624 625 // create a block parser for the XML 626 BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser(childXml, mContext, 627 false /* platformResourceFlag */); 628 629 // inflate the child without adding it to the root since we want to control where it'll 630 // get added. We do pass the parentView however to ensure that the layoutParams will 631 // be created correctly. 632 final View child = mInflater.inflate(blockParser, parentView, false /*attachToRoot*/); 633 634 invalidateRenderingSize(); 635 636 if (listener != null) { 637 new AnimationThread(this, "insertChild", listener) { 638 639 @Override 640 public Result preAnimation() { 641 parentView.setLayoutTransition(new LayoutTransition()); 642 return addView(parentView, child, index); 643 } 644 645 @Override 646 public void postAnimation() { 647 parentView.setLayoutTransition(null); 648 } 649 }.start(); 650 651 // always return success since the real status will come through the listener. 652 return SUCCESS.createResult(child); 653 } 654 655 // add it to the parentView in the correct location 656 Result result = addView(parentView, child, index); 657 if (result.isSuccess() == false) { 658 return result; 659 } 660 661 result = render(false /*freshRender*/); 662 if (result.isSuccess()) { 663 result = result.getCopyWithData(child); 664 } 665 666 return result; 667 } 668 669 /** 670 * Adds a given view to a given parent at a given index. 671 * 672 * @param parent the parent to receive the view 673 * @param view the view to add to the parent 674 * @param index the index where to do the add. 675 * 676 * @return a Result with {@link Status#SUCCESS} or 677 * {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support 678 * adding views. 679 */ 680 private Result addView(ViewGroup parent, View view, int index) { 681 try { 682 parent.addView(view, index); 683 return SUCCESS.createResult(); 684 } catch (UnsupportedOperationException e) { 685 // looks like this is a view class that doesn't support children manipulation! 686 return ERROR_VIEWGROUP_NO_CHILDREN.createResult(); 687 } 688 } 689 690 /** 691 * Moves a view to a new parent at a given location 692 * <p> 693 * {@link #acquire(long)} must have been called before this. 694 * 695 * @throws IllegalStateException if the current context is different than the one owned by 696 * the scene, or if {@link #acquire(long)} was not called. 697 * 698 * @see LayoutScene#moveChild(Object, Object, int, Map, IAnimationListener) 699 */ 700 public Result moveChild(final ViewGroup newParentView, final View childView, final int index, 701 Map<String, String> layoutParamsMap, final IAnimationListener listener) { 702 checkLock(); 703 704 invalidateRenderingSize(); 705 706 LayoutParams layoutParams = null; 707 if (layoutParamsMap != null) { 708 // need to create a new LayoutParams object for the new parent. 709 layoutParams = newParentView.generateLayoutParams( 710 new BridgeLayoutParamsMapAttributes(layoutParamsMap)); 711 } 712 713 // get the current parent of the view that needs to be moved. 714 final ViewGroup previousParent = (ViewGroup) childView.getParent(); 715 716 if (listener != null) { 717 final LayoutParams params = layoutParams; 718 719 // there is no support for animating views across layouts, so in case the new and old 720 // parent views are different we fake the animation through a no animation thread. 721 if (previousParent != newParentView) { 722 new Thread("not animated moveChild") { 723 @Override 724 public void run() { 725 Result result = moveView(previousParent, newParentView, childView, index, 726 params); 727 if (result.isSuccess() == false) { 728 listener.done(result); 729 } 730 731 // ready to do the work, acquire the scene. 732 result = acquire(250); 733 if (result.isSuccess() == false) { 734 listener.done(result); 735 return; 736 } 737 738 try { 739 result = render(false /*freshRender*/); 740 if (result.isSuccess()) { 741 listener.onNewFrame(RenderSessionImpl.this.getSession()); 742 } 743 } finally { 744 release(); 745 } 746 747 listener.done(result); 748 } 749 }.start(); 750 } else { 751 new AnimationThread(this, "moveChild", listener) { 752 753 @Override 754 public Result preAnimation() { 755 // set up the transition for the parent. 756 LayoutTransition transition = new LayoutTransition(); 757 previousParent.setLayoutTransition(transition); 758 759 // tweak the animation durations and start delays (to match the duration of 760 // animation playing just before). 761 // Note: Cannot user Animation.setDuration() directly. Have to set it 762 // on the LayoutTransition. 763 transition.setDuration(LayoutTransition.DISAPPEARING, 100); 764 // CHANGE_DISAPPEARING plays after DISAPPEARING 765 transition.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 100); 766 767 transition.setDuration(LayoutTransition.CHANGE_DISAPPEARING, 100); 768 769 transition.setDuration(LayoutTransition.CHANGE_APPEARING, 100); 770 // CHANGE_APPEARING plays after CHANGE_APPEARING 771 transition.setStartDelay(LayoutTransition.APPEARING, 100); 772 773 transition.setDuration(LayoutTransition.APPEARING, 100); 774 775 return moveView(previousParent, newParentView, childView, index, params); 776 } 777 778 @Override 779 public void postAnimation() { 780 previousParent.setLayoutTransition(null); 781 newParentView.setLayoutTransition(null); 782 } 783 }.start(); 784 } 785 786 // always return success since the real status will come through the listener. 787 return SUCCESS.createResult(layoutParams); 788 } 789 790 Result result = moveView(previousParent, newParentView, childView, index, layoutParams); 791 if (result.isSuccess() == false) { 792 return result; 793 } 794 795 result = render(false /*freshRender*/); 796 if (layoutParams != null && result.isSuccess()) { 797 result = result.getCopyWithData(layoutParams); 798 } 799 800 return result; 801 } 802 803 /** 804 * Moves a View from its current parent to a new given parent at a new given location, with 805 * an optional new {@link LayoutParams} instance 806 * 807 * @param previousParent the previous parent, still owning the child at the time of the call. 808 * @param newParent the new parent 809 * @param movedView the view to move 810 * @param index the new location in the new parent 811 * @param params an option (can be null) {@link LayoutParams} instance. 812 * 813 * @return a Result with {@link Status#SUCCESS} or 814 * {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support 815 * adding views. 816 */ 817 private Result moveView(ViewGroup previousParent, final ViewGroup newParent, 818 final View movedView, final int index, final LayoutParams params) { 819 try { 820 // check if there is a transition on the previousParent. 821 LayoutTransition previousTransition = previousParent.getLayoutTransition(); 822 if (previousTransition != null) { 823 // in this case there is an animation. This means we have to wait for the child's 824 // parent reference to be null'ed out so that we can add it to the new parent. 825 // It is technically removed right before the DISAPPEARING animation is done (if 826 // the animation of this type is not null, otherwise it's after which is impossible 827 // to handle). 828 // Because there is no move animation, if the new parent is the same as the old 829 // parent, we need to wait until the CHANGE_DISAPPEARING animation is done before 830 // adding the child or the child will appear in its new location before the 831 // other children have made room for it. 832 833 // add a listener to the transition to be notified of the actual removal. 834 previousTransition.addTransitionListener(new TransitionListener() { 835 private int mChangeDisappearingCount = 0; 836 837 public void startTransition(LayoutTransition transition, ViewGroup container, 838 View view, int transitionType) { 839 if (transitionType == LayoutTransition.CHANGE_DISAPPEARING) { 840 mChangeDisappearingCount++; 841 } 842 } 843 844 public void endTransition(LayoutTransition transition, ViewGroup container, 845 View view, int transitionType) { 846 if (transitionType == LayoutTransition.CHANGE_DISAPPEARING) { 847 mChangeDisappearingCount--; 848 } 849 850 if (transitionType == LayoutTransition.CHANGE_DISAPPEARING && 851 mChangeDisappearingCount == 0) { 852 // add it to the parentView in the correct location 853 if (params != null) { 854 newParent.addView(movedView, index, params); 855 } else { 856 newParent.addView(movedView, index); 857 } 858 } 859 } 860 }); 861 862 // remove the view from the current parent. 863 previousParent.removeView(movedView); 864 865 // and return since adding the view to the new parent is done in the listener. 866 return SUCCESS.createResult(); 867 } else { 868 // standard code with no animation. pretty simple. 869 previousParent.removeView(movedView); 870 871 // add it to the parentView in the correct location 872 if (params != null) { 873 newParent.addView(movedView, index, params); 874 } else { 875 newParent.addView(movedView, index); 876 } 877 878 return SUCCESS.createResult(); 879 } 880 } catch (UnsupportedOperationException e) { 881 // looks like this is a view class that doesn't support children manipulation! 882 return ERROR_VIEWGROUP_NO_CHILDREN.createResult(); 883 } 884 } 885 886 /** 887 * Removes a child from its current parent. 888 * <p> 889 * {@link #acquire(long)} must have been called before this. 890 * 891 * @throws IllegalStateException if the current context is different than the one owned by 892 * the scene, or if {@link #acquire(long)} was not called. 893 * 894 * @see LayoutScene#removeChild(Object, IAnimationListener) 895 */ 896 public Result removeChild(final View childView, IAnimationListener listener) { 897 checkLock(); 898 899 invalidateRenderingSize(); 900 901 final ViewGroup parent = (ViewGroup) childView.getParent(); 902 903 if (listener != null) { 904 new AnimationThread(this, "moveChild", listener) { 905 906 @Override 907 public Result preAnimation() { 908 parent.setLayoutTransition(new LayoutTransition()); 909 return removeView(parent, childView); 910 } 911 912 @Override 913 public void postAnimation() { 914 parent.setLayoutTransition(null); 915 } 916 }.start(); 917 918 // always return success since the real status will come through the listener. 919 return SUCCESS.createResult(); 920 } 921 922 Result result = removeView(parent, childView); 923 if (result.isSuccess() == false) { 924 return result; 925 } 926 927 return render(false /*freshRender*/); 928 } 929 930 /** 931 * Removes a given view from its current parent. 932 * 933 * @param view the view to remove from its parent 934 * 935 * @return a Result with {@link Status#SUCCESS} or 936 * {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support 937 * adding views. 938 */ 939 private Result removeView(ViewGroup parent, View view) { 940 try { 941 parent.removeView(view); 942 return SUCCESS.createResult(); 943 } catch (UnsupportedOperationException e) { 944 // looks like this is a view class that doesn't support children manipulation! 945 return ERROR_VIEWGROUP_NO_CHILDREN.createResult(); 946 } 947 } 948 949 /** 950 * Returns the log associated with the session. 951 * @return the log or null if there are none. 952 */ 953 public LayoutLog getLog() { 954 if (mParams != null) { 955 return mParams.getLog(); 956 } 957 958 return null; 959 } 960 961 /** 962 * Checks that the lock is owned by the current thread and that the current context is the one 963 * from this scene. 964 * 965 * @throws IllegalStateException if the current context is different than the one owned by 966 * the scene, or if {@link #acquire(long)} was not called. 967 */ 968 private void checkLock() { 969 ReentrantLock lock = Bridge.getLock(); 970 if (lock.isHeldByCurrentThread() == false) { 971 throw new IllegalStateException("scene must be acquired first. see #acquire(long)"); 972 } 973 if (sCurrentContext != mContext) { 974 throw new IllegalStateException("Thread acquired a scene but is rendering a different one"); 975 } 976 } 977 978 private void findBackground(RenderResources resources) { 979 if (mParams.isBgColorOverridden() == false) { 980 mWindowBackground = resources.findItemInTheme("windowBackground"); 981 if (mWindowBackground != null) { 982 mWindowBackground = resources.resolveResValue(mWindowBackground); 983 } 984 } 985 } 986 987 private boolean isTabletUi() { 988 return mParams.getConfigScreenSize() == ScreenSize.XLARGE; 989 } 990 991 private boolean isHCApp() { 992 RenderResources resources = mContext.getRenderResources(); 993 994 // the app must say it targets 11+ and the theme name must extend Theme.Holo or 995 // Theme.Holo.Light (which does not extend Theme.Holo, but Theme.Light) 996 if (mParams.getTargetSdkVersion() < 11) { 997 return false; 998 } 999 1000 StyleResourceValue currentTheme = resources.getCurrentTheme(); 1001 StyleResourceValue holoTheme = resources.getTheme("Theme.Holo", true /*frameworkTheme*/); 1002 1003 if (currentTheme == holoTheme || 1004 resources.themeIsParentOf(holoTheme, currentTheme)) { 1005 return true; 1006 } 1007 1008 StyleResourceValue holoLightTheme = resources.getTheme("Theme.Holo.Light", 1009 true /*frameworkTheme*/); 1010 1011 if (currentTheme == holoLightTheme || 1012 resources.themeIsParentOf(holoLightTheme, currentTheme)) { 1013 return true; 1014 } 1015 1016 return false; 1017 } 1018 1019 private void findStatusBar(RenderResources resources, DisplayMetrics metrics) { 1020 if (isTabletUi() == false) { 1021 boolean windowFullscreen = getBooleanThemeValue(resources, 1022 "windowFullscreen", false /*defaultValue*/); 1023 1024 if (windowFullscreen == false) { 1025 // default value 1026 mStatusBarSize = DEFAULT_STATUS_BAR_HEIGHT; 1027 1028 // get the real value 1029 ResourceValue value = resources.getFrameworkResource(ResourceType.DIMEN, 1030 "status_bar_height"); 1031 1032 if (value != null) { 1033 TypedValue typedValue = ResourceHelper.getValue(value.getValue()); 1034 if (typedValue != null) { 1035 // compute the pixel value based on the display metrics 1036 mStatusBarSize = (int)typedValue.getDimension(metrics); 1037 } 1038 } 1039 } 1040 } 1041 } 1042 1043 private void findTopBar(RenderResources resources, DisplayMetrics metrics) { 1044 boolean windowIsFloating = getBooleanThemeValue(resources, 1045 "windowIsFloating", true /*defaultValue*/); 1046 1047 if (windowIsFloating == false) { 1048 if (isHCApp()) { 1049 findActionBar(resources, metrics); 1050 } else { 1051 findTitleBar(resources, metrics); 1052 } 1053 } 1054 } 1055 1056 private void findActionBar(RenderResources resources, DisplayMetrics metrics) { 1057 boolean windowActionBar = getBooleanThemeValue(resources, 1058 "windowActionBar", true /*defaultValue*/); 1059 1060 // if there's a value and it's false (default is true) 1061 if (windowActionBar) { 1062 1063 // default size of the window title bar 1064 mTopBarSize = DEFAULT_TITLE_BAR_HEIGHT; 1065 1066 // get value from the theme. 1067 ResourceValue value = resources.findItemInTheme("actionBarSize"); 1068 1069 // resolve it 1070 value = resources.resolveResValue(value); 1071 1072 if (value != null) { 1073 // get the numerical value, if available 1074 TypedValue typedValue = ResourceHelper.getValue(value.getValue()); 1075 if (typedValue != null) { 1076 // compute the pixel value based on the display metrics 1077 mTopBarSize = (int)typedValue.getDimension(metrics); 1078 } 1079 } 1080 } 1081 } 1082 1083 private void findTitleBar(RenderResources resources, DisplayMetrics metrics) { 1084 boolean windowNoTitle = getBooleanThemeValue(resources, 1085 "windowNoTitle", false /*defaultValue*/); 1086 1087 if (windowNoTitle == false) { 1088 1089 // default size of the window title bar 1090 mTopBarSize = DEFAULT_TITLE_BAR_HEIGHT; 1091 1092 // get value from the theme. 1093 ResourceValue value = resources.findItemInTheme("windowTitleSize"); 1094 1095 // resolve it 1096 value = resources.resolveResValue(value); 1097 1098 if (value != null) { 1099 // get the numerical value, if available 1100 TypedValue typedValue = ResourceHelper.getValue(value.getValue()); 1101 if (typedValue != null) { 1102 // compute the pixel value based on the display metrics 1103 mTopBarSize = (int)typedValue.getDimension(metrics); 1104 } 1105 } 1106 } 1107 } 1108 1109 private void findSystemBar(RenderResources resources, DisplayMetrics metrics) { 1110 if (isTabletUi() && getBooleanThemeValue( 1111 resources, "windowIsFloating", true /*defaultValue*/) == false) { 1112 1113 // default value 1114 mSystemBarSize = 56; // ?? 1115 1116 // get the real value 1117 ResourceValue value = resources.getFrameworkResource(ResourceType.DIMEN, 1118 "status_bar_height"); 1119 1120 if (value != null) { 1121 TypedValue typedValue = ResourceHelper.getValue(value.getValue()); 1122 if (typedValue != null) { 1123 // compute the pixel value based on the display metrics 1124 mSystemBarSize = (int)typedValue.getDimension(metrics); 1125 } 1126 } 1127 } 1128 } 1129 1130 private boolean getBooleanThemeValue(RenderResources resources, 1131 String name, boolean defaultValue) { 1132 1133 // get the title bar flag from the current theme. 1134 ResourceValue value = resources.findItemInTheme(name); 1135 1136 // because it may reference something else, we resolve it. 1137 value = resources.resolveResValue(value); 1138 1139 // if there's no value, return the default. 1140 if (value == null || value.getValue() == null) { 1141 return defaultValue; 1142 } 1143 1144 return XmlUtils.convertValueToBoolean(value.getValue(), defaultValue); 1145 } 1146 1147 /** 1148 * Post process on a view hierachy that was just inflated. 1149 * <p/>At the moment this only support TabHost: If {@link TabHost} is detected, look for the 1150 * {@link TabWidget}, and the corresponding {@link FrameLayout} and make new tabs automatically 1151 * based on the content of the {@link FrameLayout}. 1152 * @param view the root view to process. 1153 * @param projectCallback callback to the project. 1154 */ 1155 private void postInflateProcess(View view, IProjectCallback projectCallback) 1156 throws PostInflateException { 1157 if (view instanceof TabHost) { 1158 setupTabHost((TabHost)view, projectCallback); 1159 } else if (view instanceof QuickContactBadge) { 1160 QuickContactBadge badge = (QuickContactBadge) view; 1161 badge.setImageToDefault(); 1162 } else if (view instanceof ViewGroup) { 1163 ViewGroup group = (ViewGroup)view; 1164 final int count = group.getChildCount(); 1165 for (int c = 0 ; c < count ; c++) { 1166 View child = group.getChildAt(c); 1167 postInflateProcess(child, projectCallback); 1168 } 1169 } 1170 } 1171 1172 /** 1173 * Sets up a {@link TabHost} object. 1174 * @param tabHost the TabHost to setup. 1175 * @param projectCallback The project callback object to access the project R class. 1176 * @throws PostInflateException 1177 */ 1178 private void setupTabHost(TabHost tabHost, IProjectCallback projectCallback) 1179 throws PostInflateException { 1180 // look for the TabWidget, and the FrameLayout. They have their own specific names 1181 View v = tabHost.findViewById(android.R.id.tabs); 1182 1183 if (v == null) { 1184 throw new PostInflateException( 1185 "TabHost requires a TabWidget with id \"android:id/tabs\".\n"); 1186 } 1187 1188 if ((v instanceof TabWidget) == false) { 1189 throw new PostInflateException(String.format( 1190 "TabHost requires a TabWidget with id \"android:id/tabs\".\n" + 1191 "View found with id 'tabs' is '%s'", v.getClass().getCanonicalName())); 1192 } 1193 1194 v = tabHost.findViewById(android.R.id.tabcontent); 1195 1196 if (v == null) { 1197 // TODO: see if we can fake tabs even without the FrameLayout (same below when the framelayout is empty) 1198 throw new PostInflateException( 1199 "TabHost requires a FrameLayout with id \"android:id/tabcontent\"."); 1200 } 1201 1202 if ((v instanceof FrameLayout) == false) { 1203 throw new PostInflateException(String.format( 1204 "TabHost requires a FrameLayout with id \"android:id/tabcontent\".\n" + 1205 "View found with id 'tabcontent' is '%s'", v.getClass().getCanonicalName())); 1206 } 1207 1208 FrameLayout content = (FrameLayout)v; 1209 1210 // now process the content of the framelayout and dynamically create tabs for it. 1211 final int count = content.getChildCount(); 1212 1213 // this must be called before addTab() so that the TabHost searches its TabWidget 1214 // and FrameLayout. 1215 tabHost.setup(); 1216 1217 if (count == 0) { 1218 // Create a dummy child to get a single tab 1219 TabSpec spec = tabHost.newTabSpec("tag").setIndicator("Tab Label", 1220 tabHost.getResources().getDrawable(android.R.drawable.ic_menu_info_details)) 1221 .setContent(new TabHost.TabContentFactory() { 1222 public View createTabContent(String tag) { 1223 return new LinearLayout(mContext); 1224 } 1225 }); 1226 tabHost.addTab(spec); 1227 return; 1228 } else { 1229 // for each child of the framelayout, add a new TabSpec 1230 for (int i = 0 ; i < count ; i++) { 1231 View child = content.getChildAt(i); 1232 String tabSpec = String.format("tab_spec%d", i+1); 1233 int id = child.getId(); 1234 Pair<ResourceType, String> resource = projectCallback.resolveResourceId(id); 1235 String name; 1236 if (resource != null) { 1237 name = resource.getSecond(); 1238 } else { 1239 name = String.format("Tab %d", i+1); // default name if id is unresolved. 1240 } 1241 tabHost.addTab(tabHost.newTabSpec(tabSpec).setIndicator(name).setContent(id)); 1242 } 1243 } 1244 } 1245 1246 1247 /** 1248 * Visits a View and its children and generate a {@link ViewInfo} containing the 1249 * bounds of all the views. 1250 * @param view the root View 1251 * @param context the context. 1252 */ 1253 private ViewInfo visit(View view, BridgeContext context, int offset) { 1254 if (view == null) { 1255 return null; 1256 } 1257 1258 ViewInfo result = new ViewInfo(view.getClass().getName(), 1259 context.getViewKey(view), 1260 view.getLeft(), view.getTop() + offset, view.getRight(), view.getBottom() + offset, 1261 view, view.getLayoutParams()); 1262 1263 if (view instanceof ViewGroup) { 1264 ViewGroup group = ((ViewGroup) view); 1265 result.setChildren(visitAllChildren(group, context, 0 /*offset*/)); 1266 } 1267 1268 return result; 1269 } 1270 1271 private List<ViewInfo> visitAllChildren(ViewGroup viewGroup, BridgeContext context, 1272 int offset) { 1273 if (viewGroup == null) { 1274 return null; 1275 } 1276 1277 List<ViewInfo> children = new ArrayList<ViewInfo>(); 1278 for (int i = 0; i < viewGroup.getChildCount(); i++) { 1279 children.add(visit(viewGroup.getChildAt(i), context, offset)); 1280 } 1281 return children; 1282 } 1283 1284 1285 private void invalidateRenderingSize() { 1286 mMeasuredScreenWidth = mMeasuredScreenHeight = -1; 1287 } 1288 1289 public BufferedImage getImage() { 1290 return mImage; 1291 } 1292 1293 public boolean isAlphaChannelImage() { 1294 return mIsAlphaChannelImage; 1295 } 1296 1297 public List<ViewInfo> getViewInfos() { 1298 return mViewInfoList; 1299 } 1300 1301 public Map<String, String> getDefaultProperties(Object viewObject) { 1302 return mContext.getDefaultPropMap(viewObject); 1303 } 1304 1305 public void setScene(RenderSession session) { 1306 mScene = session; 1307 } 1308 1309 public RenderSession getSession() { 1310 return mScene; 1311 } 1312 1313 // --- FrameworkResourceIdProvider methods 1314 1315 @Override 1316 public Integer getId(ResourceType resType, String resName) { 1317 return Bridge.getResourceId(resType, resName); 1318 } 1319} 1320