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