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