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