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