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