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