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