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