1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.launcher2; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.AnimatorSet; 22 import android.animation.ObjectAnimator; 23 import android.content.Context; 24 import android.content.res.Resources; 25 import android.util.AttributeSet; 26 import android.view.LayoutInflater; 27 import android.view.MotionEvent; 28 import android.view.View; 29 import android.view.ViewGroup; 30 import android.widget.FrameLayout; 31 import android.widget.LinearLayout; 32 import android.widget.TabHost; 33 import android.widget.TabWidget; 34 import android.widget.TextView; 35 36 import com.android.launcher.R; 37 38 import java.util.ArrayList; 39 40 public class AppsCustomizeTabHost extends TabHost implements LauncherTransitionable, 41 TabHost.OnTabChangeListener { 42 static final String LOG_TAG = "AppsCustomizeTabHost"; 43 44 private static final String APPS_TAB_TAG = "APPS"; 45 private static final String WIDGETS_TAB_TAG = "WIDGETS"; 46 47 private final LayoutInflater mLayoutInflater; 48 private ViewGroup mTabs; 49 private ViewGroup mTabsContainer; 50 private AppsCustomizePagedView mAppsCustomizePane; 51 private FrameLayout mAnimationBuffer; 52 private LinearLayout mContent; 53 54 private boolean mInTransition; 55 private boolean mTransitioningToWorkspace; 56 private boolean mResetAfterTransition; 57 private Runnable mRelayoutAndMakeVisible; 58 AppsCustomizeTabHost(Context context, AttributeSet attrs)59 public AppsCustomizeTabHost(Context context, AttributeSet attrs) { 60 super(context, attrs); 61 mLayoutInflater = LayoutInflater.from(context); 62 mRelayoutAndMakeVisible = new Runnable() { 63 public void run() { 64 mTabs.requestLayout(); 65 mTabsContainer.setAlpha(1f); 66 } 67 }; 68 } 69 70 /** 71 * Convenience methods to select specific tabs. We want to set the content type immediately 72 * in these cases, but we note that we still call setCurrentTabByTag() so that the tab view 73 * reflects the new content (but doesn't do the animation and logic associated with changing 74 * tabs manually). 75 */ setContentTypeImmediate(AppsCustomizePagedView.ContentType type)76 void setContentTypeImmediate(AppsCustomizePagedView.ContentType type) { 77 setOnTabChangedListener(null); 78 onTabChangedStart(); 79 onTabChangedEnd(type); 80 setCurrentTabByTag(getTabTagForContentType(type)); 81 setOnTabChangedListener(this); 82 } selectAppsTab()83 void selectAppsTab() { 84 setContentTypeImmediate(AppsCustomizePagedView.ContentType.Applications); 85 } selectWidgetsTab()86 void selectWidgetsTab() { 87 setContentTypeImmediate(AppsCustomizePagedView.ContentType.Widgets); 88 } 89 90 /** 91 * Setup the tab host and create all necessary tabs. 92 */ 93 @Override onFinishInflate()94 protected void onFinishInflate() { 95 // Setup the tab host 96 setup(); 97 98 final ViewGroup tabsContainer = (ViewGroup) findViewById(R.id.tabs_container); 99 final TabWidget tabs = getTabWidget(); 100 final AppsCustomizePagedView appsCustomizePane = (AppsCustomizePagedView) 101 findViewById(R.id.apps_customize_pane_content); 102 mTabs = tabs; 103 mTabsContainer = tabsContainer; 104 mAppsCustomizePane = appsCustomizePane; 105 mAnimationBuffer = (FrameLayout) findViewById(R.id.animation_buffer); 106 mContent = (LinearLayout) findViewById(R.id.apps_customize_content); 107 if (tabs == null || mAppsCustomizePane == null) throw new Resources.NotFoundException(); 108 109 // Configure the tabs content factory to return the same paged view (that we change the 110 // content filter on) 111 TabContentFactory contentFactory = new TabContentFactory() { 112 public View createTabContent(String tag) { 113 return appsCustomizePane; 114 } 115 }; 116 117 // Create the tabs 118 TextView tabView; 119 String label; 120 label = getContext().getString(R.string.all_apps_button_label); 121 tabView = (TextView) mLayoutInflater.inflate(R.layout.tab_widget_indicator, tabs, false); 122 tabView.setText(label); 123 tabView.setContentDescription(label); 124 addTab(newTabSpec(APPS_TAB_TAG).setIndicator(tabView).setContent(contentFactory)); 125 label = getContext().getString(R.string.widgets_tab_label); 126 tabView = (TextView) mLayoutInflater.inflate(R.layout.tab_widget_indicator, tabs, false); 127 tabView.setText(label); 128 tabView.setContentDescription(label); 129 addTab(newTabSpec(WIDGETS_TAB_TAG).setIndicator(tabView).setContent(contentFactory)); 130 setOnTabChangedListener(this); 131 132 // Setup the key listener to jump between the last tab view and the market icon 133 AppsCustomizeTabKeyEventListener keyListener = new AppsCustomizeTabKeyEventListener(); 134 View lastTab = tabs.getChildTabViewAt(tabs.getTabCount() - 1); 135 lastTab.setOnKeyListener(keyListener); 136 View shopButton = findViewById(R.id.market_button); 137 shopButton.setOnKeyListener(keyListener); 138 139 // Hide the tab bar until we measure 140 mTabsContainer.setAlpha(0f); 141 } 142 143 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)144 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 145 boolean remeasureTabWidth = (mTabs.getLayoutParams().width <= 0); 146 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 147 148 // Set the width of the tab list to the content width 149 if (remeasureTabWidth) { 150 int contentWidth = mAppsCustomizePane.getPageContentWidth(); 151 if (contentWidth > 0 && mTabs.getLayoutParams().width != contentWidth) { 152 // Set the width and show the tab bar 153 mTabs.getLayoutParams().width = contentWidth; 154 mRelayoutAndMakeVisible.run(); 155 } 156 157 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 158 } 159 } 160 onInterceptTouchEvent(MotionEvent ev)161 public boolean onInterceptTouchEvent(MotionEvent ev) { 162 // If we are mid transitioning to the workspace, then intercept touch events here so we 163 // can ignore them, otherwise we just let all apps handle the touch events. 164 if (mInTransition && mTransitioningToWorkspace) { 165 return true; 166 } 167 return super.onInterceptTouchEvent(ev); 168 }; 169 170 @Override onTouchEvent(MotionEvent event)171 public boolean onTouchEvent(MotionEvent event) { 172 // Allow touch events to fall through to the workspace if we are transitioning there 173 if (mInTransition && mTransitioningToWorkspace) { 174 return super.onTouchEvent(event); 175 } 176 177 // Intercept all touch events up to the bottom of the AppsCustomizePane so they do not fall 178 // through to the workspace and trigger showWorkspace() 179 if (event.getY() < mAppsCustomizePane.getBottom()) { 180 return true; 181 } 182 return super.onTouchEvent(event); 183 } 184 onTabChangedStart()185 private void onTabChangedStart() { 186 mAppsCustomizePane.hideScrollingIndicator(false); 187 } 188 reloadCurrentPage()189 private void reloadCurrentPage() { 190 if (!LauncherApplication.isScreenLarge()) { 191 mAppsCustomizePane.flashScrollingIndicator(true); 192 } 193 mAppsCustomizePane.loadAssociatedPages(mAppsCustomizePane.getCurrentPage()); 194 mAppsCustomizePane.requestFocus(); 195 } 196 onTabChangedEnd(AppsCustomizePagedView.ContentType type)197 private void onTabChangedEnd(AppsCustomizePagedView.ContentType type) { 198 mAppsCustomizePane.setContentType(type); 199 } 200 201 @Override onTabChanged(String tabId)202 public void onTabChanged(String tabId) { 203 final AppsCustomizePagedView.ContentType type = getContentTypeForTabTag(tabId); 204 205 // Animate the changing of the tab content by fading pages in and out 206 final Resources res = getResources(); 207 final int duration = res.getInteger(R.integer.config_tabTransitionDuration); 208 209 // We post a runnable here because there is a delay while the first page is loading and 210 // the feedback from having changed the tab almost feels better than having it stick 211 post(new Runnable() { 212 @Override 213 public void run() { 214 if (mAppsCustomizePane.getMeasuredWidth() <= 0 || 215 mAppsCustomizePane.getMeasuredHeight() <= 0) { 216 reloadCurrentPage(); 217 return; 218 } 219 220 // Take the visible pages and re-parent them temporarily to mAnimatorBuffer 221 // and then cross fade to the new pages 222 int[] visiblePageRange = new int[2]; 223 mAppsCustomizePane.getVisiblePages(visiblePageRange); 224 if (visiblePageRange[0] == -1 && visiblePageRange[1] == -1) { 225 // If we can't get the visible page ranges, then just skip the animation 226 reloadCurrentPage(); 227 return; 228 } 229 ArrayList<View> visiblePages = new ArrayList<View>(); 230 for (int i = visiblePageRange[0]; i <= visiblePageRange[1]; i++) { 231 visiblePages.add(mAppsCustomizePane.getPageAt(i)); 232 } 233 234 // We want the pages to be rendered in exactly the same way as they were when 235 // their parent was mAppsCustomizePane -- so set the scroll on mAnimationBuffer 236 // to be exactly the same as mAppsCustomizePane, and below, set the left/top 237 // parameters to be correct for each of the pages 238 mAnimationBuffer.scrollTo(mAppsCustomizePane.getScrollX(), 0); 239 240 // mAppsCustomizePane renders its children in reverse order, so 241 // add the pages to mAnimationBuffer in reverse order to match that behavior 242 for (int i = visiblePages.size() - 1; i >= 0; i--) { 243 View child = visiblePages.get(i); 244 if (child instanceof PagedViewCellLayout) { 245 ((PagedViewCellLayout) child).resetChildrenOnKeyListeners(); 246 } else if (child instanceof PagedViewGridLayout) { 247 ((PagedViewGridLayout) child).resetChildrenOnKeyListeners(); 248 } 249 PagedViewWidget.setDeletePreviewsWhenDetachedFromWindow(false); 250 mAppsCustomizePane.removeView(child); 251 PagedViewWidget.setDeletePreviewsWhenDetachedFromWindow(true); 252 mAnimationBuffer.setAlpha(1f); 253 mAnimationBuffer.setVisibility(View.VISIBLE); 254 LayoutParams p = new FrameLayout.LayoutParams(child.getMeasuredWidth(), 255 child.getMeasuredHeight()); 256 p.setMargins((int) child.getLeft(), (int) child.getTop(), 0, 0); 257 mAnimationBuffer.addView(child, p); 258 } 259 260 // Toggle the new content 261 onTabChangedStart(); 262 onTabChangedEnd(type); 263 264 // Animate the transition 265 ObjectAnimator outAnim = LauncherAnimUtils.ofFloat(mAnimationBuffer, "alpha", 0f); 266 outAnim.addListener(new AnimatorListenerAdapter() { 267 private void clearAnimationBuffer() { 268 mAnimationBuffer.setVisibility(View.GONE); 269 PagedViewWidget.setRecyclePreviewsWhenDetachedFromWindow(false); 270 mAnimationBuffer.removeAllViews(); 271 PagedViewWidget.setRecyclePreviewsWhenDetachedFromWindow(true); 272 } 273 @Override 274 public void onAnimationEnd(Animator animation) { 275 clearAnimationBuffer(); 276 } 277 @Override 278 public void onAnimationCancel(Animator animation) { 279 clearAnimationBuffer(); 280 } 281 }); 282 ObjectAnimator inAnim = LauncherAnimUtils.ofFloat(mAppsCustomizePane, "alpha", 1f); 283 inAnim.addListener(new AnimatorListenerAdapter() { 284 @Override 285 public void onAnimationEnd(Animator animation) { 286 reloadCurrentPage(); 287 } 288 }); 289 290 final AnimatorSet animSet = LauncherAnimUtils.createAnimatorSet(); 291 animSet.playTogether(outAnim, inAnim); 292 animSet.setDuration(duration); 293 animSet.start(); 294 } 295 }); 296 } 297 setCurrentTabFromContent(AppsCustomizePagedView.ContentType type)298 public void setCurrentTabFromContent(AppsCustomizePagedView.ContentType type) { 299 setOnTabChangedListener(null); 300 setCurrentTabByTag(getTabTagForContentType(type)); 301 setOnTabChangedListener(this); 302 } 303 304 /** 305 * Returns the content type for the specified tab tag. 306 */ getContentTypeForTabTag(String tag)307 public AppsCustomizePagedView.ContentType getContentTypeForTabTag(String tag) { 308 if (tag.equals(APPS_TAB_TAG)) { 309 return AppsCustomizePagedView.ContentType.Applications; 310 } else if (tag.equals(WIDGETS_TAB_TAG)) { 311 return AppsCustomizePagedView.ContentType.Widgets; 312 } 313 return AppsCustomizePagedView.ContentType.Applications; 314 } 315 316 /** 317 * Returns the tab tag for a given content type. 318 */ getTabTagForContentType(AppsCustomizePagedView.ContentType type)319 public String getTabTagForContentType(AppsCustomizePagedView.ContentType type) { 320 if (type == AppsCustomizePagedView.ContentType.Applications) { 321 return APPS_TAB_TAG; 322 } else if (type == AppsCustomizePagedView.ContentType.Widgets) { 323 return WIDGETS_TAB_TAG; 324 } 325 return APPS_TAB_TAG; 326 } 327 328 /** 329 * Disable focus on anything under this view in the hierarchy if we are not visible. 330 */ 331 @Override getDescendantFocusability()332 public int getDescendantFocusability() { 333 if (getVisibility() != View.VISIBLE) { 334 return ViewGroup.FOCUS_BLOCK_DESCENDANTS; 335 } 336 return super.getDescendantFocusability(); 337 } 338 reset()339 void reset() { 340 if (mInTransition) { 341 // Defer to after the transition to reset 342 mResetAfterTransition = true; 343 } else { 344 // Reset immediately 345 mAppsCustomizePane.reset(); 346 } 347 } 348 enableAndBuildHardwareLayer()349 private void enableAndBuildHardwareLayer() { 350 // isHardwareAccelerated() checks if we're attached to a window and if that 351 // window is HW accelerated-- we were sometimes not attached to a window 352 // and buildLayer was throwing an IllegalStateException 353 if (isHardwareAccelerated()) { 354 // Turn on hardware layers for performance 355 setLayerType(LAYER_TYPE_HARDWARE, null); 356 357 // force building the layer, so you don't get a blip early in an animation 358 // when the layer is created layer 359 buildLayer(); 360 } 361 } 362 363 @Override getContent()364 public View getContent() { 365 return mContent; 366 } 367 368 /* LauncherTransitionable overrides */ 369 @Override onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace)370 public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) { 371 mAppsCustomizePane.onLauncherTransitionPrepare(l, animated, toWorkspace); 372 mInTransition = true; 373 mTransitioningToWorkspace = toWorkspace; 374 375 if (toWorkspace) { 376 // Going from All Apps -> Workspace 377 setVisibilityOfSiblingsWithLowerZOrder(VISIBLE); 378 // Stop the scrolling indicator - we don't want All Apps to be invalidating itself 379 // during the transition, especially since it has a hardware layer set on it 380 mAppsCustomizePane.cancelScrollingIndicatorAnimations(); 381 } else { 382 // Going from Workspace -> All Apps 383 mContent.setVisibility(VISIBLE); 384 385 // Make sure the current page is loaded (we start loading the side pages after the 386 // transition to prevent slowing down the animation) 387 mAppsCustomizePane.loadAssociatedPages(mAppsCustomizePane.getCurrentPage(), true); 388 389 if (!LauncherApplication.isScreenLarge()) { 390 mAppsCustomizePane.showScrollingIndicator(true); 391 } 392 } 393 394 if (mResetAfterTransition) { 395 mAppsCustomizePane.reset(); 396 mResetAfterTransition = false; 397 } 398 } 399 400 @Override onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace)401 public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) { 402 if (animated) { 403 enableAndBuildHardwareLayer(); 404 } 405 } 406 407 @Override onLauncherTransitionStep(Launcher l, float t)408 public void onLauncherTransitionStep(Launcher l, float t) { 409 // Do nothing 410 } 411 412 @Override onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace)413 public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { 414 mAppsCustomizePane.onLauncherTransitionEnd(l, animated, toWorkspace); 415 mInTransition = false; 416 if (animated) { 417 setLayerType(LAYER_TYPE_NONE, null); 418 } 419 420 if (!toWorkspace) { 421 // Dismiss the workspace cling 422 l.dismissWorkspaceCling(null); 423 // Show the all apps cling (if not already shown) 424 mAppsCustomizePane.showAllAppsCling(); 425 // Make sure adjacent pages are loaded (we wait until after the transition to 426 // prevent slowing down the animation) 427 mAppsCustomizePane.loadAssociatedPages(mAppsCustomizePane.getCurrentPage()); 428 429 if (!LauncherApplication.isScreenLarge()) { 430 mAppsCustomizePane.hideScrollingIndicator(false); 431 } 432 433 // Going from Workspace -> All Apps 434 // NOTE: We should do this at the end since we check visibility state in some of the 435 // cling initialization/dismiss code above. 436 setVisibilityOfSiblingsWithLowerZOrder(INVISIBLE); 437 } 438 } 439 setVisibilityOfSiblingsWithLowerZOrder(int visibility)440 private void setVisibilityOfSiblingsWithLowerZOrder(int visibility) { 441 ViewGroup parent = (ViewGroup) getParent(); 442 if (parent == null) return; 443 444 final int count = parent.getChildCount(); 445 if (!isChildrenDrawingOrderEnabled()) { 446 for (int i = 0; i < count; i++) { 447 final View child = parent.getChildAt(i); 448 if (child == this) { 449 break; 450 } else { 451 if (child.getVisibility() == GONE) { 452 continue; 453 } 454 child.setVisibility(visibility); 455 } 456 } 457 } else { 458 throw new RuntimeException("Failed; can't get z-order of views"); 459 } 460 } 461 onWindowVisible()462 public void onWindowVisible() { 463 if (getVisibility() == VISIBLE) { 464 mContent.setVisibility(VISIBLE); 465 // We unload the widget previews when the UI is hidden, so need to reload pages 466 // Load the current page synchronously, and the neighboring pages asynchronously 467 mAppsCustomizePane.loadAssociatedPages(mAppsCustomizePane.getCurrentPage(), true); 468 mAppsCustomizePane.loadAssociatedPages(mAppsCustomizePane.getCurrentPage()); 469 } 470 } 471 onTrimMemory()472 public void onTrimMemory() { 473 mContent.setVisibility(GONE); 474 // Clear the widget pages of all their subviews - this will trigger the widget previews 475 // to delete their bitmaps 476 mAppsCustomizePane.clearAllWidgetPages(); 477 } 478 isTransitioning()479 boolean isTransitioning() { 480 return mInTransition; 481 } 482 } 483