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