1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.statusbar.tablet; 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.graphics.Rect; 25 import android.util.AttributeSet; 26 import android.util.Slog; 27 import android.view.LayoutInflater; 28 import android.view.MotionEvent; 29 import android.view.View; 30 import android.view.ViewGroup; 31 import android.view.ViewTreeObserver; 32 import android.view.animation.AccelerateInterpolator; 33 import android.view.animation.DecelerateInterpolator; 34 import android.view.animation.Interpolator; 35 import android.widget.RelativeLayout; 36 37 import com.android.systemui.R; 38 39 public class NotificationPanel extends RelativeLayout implements StatusBarPanel, 40 View.OnClickListener { 41 static final String TAG = "Tablet/NotificationPanel"; 42 static final boolean DEBUG = false; 43 44 final static int PANEL_FADE_DURATION = 150; 45 46 boolean mShowing; 47 boolean mHasClearableNotifications = false; 48 int mNotificationCount = 0; 49 NotificationPanelTitle mTitleArea; 50 View mSettingsButton; 51 View mNotificationButton; 52 View mNotificationScroller; 53 ViewGroup mContentFrame; 54 Rect mContentArea = new Rect(); 55 View mSettingsView; 56 ViewGroup mContentParent; 57 TabletStatusBar mBar; 58 View mClearButton; 59 static Interpolator sAccelerateInterpolator = new AccelerateInterpolator(); 60 static Interpolator sDecelerateInterpolator = new DecelerateInterpolator(); 61 62 // amount to slide mContentParent down by when mContentFrame is missing 63 float mContentFrameMissingTranslation; 64 65 Choreographer mChoreo = new Choreographer(); 66 NotificationPanel(Context context, AttributeSet attrs)67 public NotificationPanel(Context context, AttributeSet attrs) { 68 this(context, attrs, 0); 69 } 70 NotificationPanel(Context context, AttributeSet attrs, int defStyle)71 public NotificationPanel(Context context, AttributeSet attrs, int defStyle) { 72 super(context, attrs, defStyle); 73 } 74 setBar(TabletStatusBar b)75 public void setBar(TabletStatusBar b) { 76 mBar = b; 77 } 78 79 @Override onFinishInflate()80 public void onFinishInflate() { 81 super.onFinishInflate(); 82 83 setWillNotDraw(false); 84 85 mContentParent = (ViewGroup)findViewById(R.id.content_parent); 86 mContentParent.bringToFront(); 87 mTitleArea = (NotificationPanelTitle) findViewById(R.id.title_area); 88 mTitleArea.setPanel(this); 89 90 mSettingsButton = findViewById(R.id.settings_button); 91 mNotificationButton = findViewById(R.id.notification_button); 92 93 mNotificationScroller = findViewById(R.id.notification_scroller); 94 mContentFrame = (ViewGroup)findViewById(R.id.content_frame); 95 mContentFrameMissingTranslation = 0; // not needed with current assets 96 97 // the "X" that appears in place of the clock when the panel is showing notifications 98 mClearButton = findViewById(R.id.clear_all_button); 99 mClearButton.setOnClickListener(mClearButtonListener); 100 101 mShowing = false; 102 103 setContentFrameVisible(mNotificationCount > 0, false); 104 } 105 106 private View.OnClickListener mClearButtonListener = new View.OnClickListener() { 107 public void onClick(View v) { 108 mBar.clearAll(); 109 } 110 }; 111 getClearButton()112 public View getClearButton() { 113 return mClearButton; 114 } 115 show(boolean show, boolean animate)116 public void show(boolean show, boolean animate) { 117 if (show && !mShowing) { 118 setContentFrameVisible(mSettingsView != null || mNotificationCount > 0, false); 119 } 120 121 if (animate) { 122 if (mShowing != show) { 123 mShowing = show; 124 if (show) { 125 setVisibility(View.VISIBLE); 126 // Don't start the animation until we've created the layer, which is done 127 // right before we are drawn 128 mContentParent.setLayerType(View.LAYER_TYPE_HARDWARE, null); 129 getViewTreeObserver().addOnPreDrawListener(mPreDrawListener); 130 } else { 131 mChoreo.startAnimation(show); 132 } 133 } 134 } else { 135 mShowing = show; 136 setVisibility(show ? View.VISIBLE : View.GONE); 137 } 138 } 139 140 /** 141 * This is used only when we've created a hardware layer and are waiting until it's 142 * been created in order to start the appearing animation. 143 */ 144 private ViewTreeObserver.OnPreDrawListener mPreDrawListener = 145 new ViewTreeObserver.OnPreDrawListener() { 146 @Override 147 public boolean onPreDraw() { 148 getViewTreeObserver().removeOnPreDrawListener(this); 149 mChoreo.startAnimation(true); 150 return false; 151 } 152 }; 153 154 /** 155 * Whether the panel is showing, or, if it's animating, whether it will be 156 * when the animation is done. 157 */ isShowing()158 public boolean isShowing() { 159 return mShowing; 160 } 161 162 @Override onVisibilityChanged(View v, int vis)163 public void onVisibilityChanged(View v, int vis) { 164 super.onVisibilityChanged(v, vis); 165 // when we hide, put back the notifications 166 if (vis != View.VISIBLE) { 167 if (mSettingsView != null) removeSettingsView(); 168 mNotificationScroller.setVisibility(View.VISIBLE); 169 mNotificationScroller.setAlpha(1f); 170 mNotificationScroller.scrollTo(0, 0); 171 updatePanelModeButtons(); 172 } 173 } 174 175 @Override dispatchHoverEvent(MotionEvent event)176 public boolean dispatchHoverEvent(MotionEvent event) { 177 // Ignore hover events outside of this panel bounds since such events 178 // generate spurious accessibility events with the panel content when 179 // tapping outside of it, thus confusing the user. 180 final int x = (int) event.getX(); 181 final int y = (int) event.getY(); 182 if (x >= 0 && x < getWidth() && y >= 0 && y < getHeight()) { 183 return super.dispatchHoverEvent(event); 184 } 185 return true; 186 } 187 188 /* 189 @Override 190 protected void onLayout(boolean changed, int l, int t, int r, int b) { 191 super.onLayout(changed, l, t, r, b); 192 193 if (DEBUG) Slog.d(TAG, String.format("PANEL: onLayout: (%d, %d, %d, %d)", l, t, r, b)); 194 } 195 196 @Override 197 public void onSizeChanged(int w, int h, int oldw, int oldh) { 198 super.onSizeChanged(w, h, oldw, oldh); 199 200 if (DEBUG) { 201 Slog.d(TAG, String.format("PANEL: onSizeChanged: (%d -> %d, %d -> %d)", 202 oldw, w, oldh, h)); 203 } 204 } 205 */ 206 onClick(View v)207 public void onClick(View v) { 208 if (v == mTitleArea) { 209 swapPanels(); 210 } 211 } 212 setNotificationCount(int n)213 public void setNotificationCount(int n) { 214 // Slog.d(TAG, "notificationCount=" + n); 215 if (!mShowing) { 216 // just do it, already 217 setContentFrameVisible(n > 0, false); 218 } else if (mSettingsView == null) { 219 // we're looking at the notifications; time to maybe make some changes 220 if ((mNotificationCount > 0) != (n > 0)) { 221 setContentFrameVisible(n > 0, true); 222 } 223 } 224 mNotificationCount = n; 225 } 226 setContentFrameVisible(final boolean showing, boolean animate)227 public void setContentFrameVisible(final boolean showing, boolean animate) { 228 if (!animate) { 229 mContentFrame.setVisibility(showing ? View.VISIBLE : View.GONE); 230 mContentFrame.setAlpha(1f); 231 // the translation will be patched up when the window is slid into place 232 return; 233 } 234 235 if (showing) { 236 mContentFrame.setVisibility(View.VISIBLE); 237 } 238 AnimatorSet set = new AnimatorSet(); 239 set.play(ObjectAnimator.ofFloat( 240 mContentFrame, "alpha", 241 showing ? 0f : 1f, 242 showing ? 1f : 0f)) 243 .with(ObjectAnimator.ofFloat( 244 mContentParent, "translationY", 245 showing ? mContentFrameMissingTranslation : 0f, 246 showing ? 0f : mContentFrameMissingTranslation)) 247 ; 248 249 set.setDuration(200); 250 set.addListener(new AnimatorListenerAdapter() { 251 @Override 252 public void onAnimationEnd(Animator _a) { 253 if (!showing) { 254 mContentFrame.setVisibility(View.GONE); 255 mContentFrame.setAlpha(1f); 256 } 257 updateClearButton(); 258 } 259 }); 260 set.start(); 261 } 262 swapPanels()263 public void swapPanels() { 264 final View toShow, toHide; 265 if (mSettingsView == null) { 266 addSettingsView(); 267 toShow = mSettingsView; 268 toHide = mNotificationScroller; 269 } else { 270 toShow = mNotificationScroller; 271 toHide = mSettingsView; 272 } 273 Animator a = ObjectAnimator.ofFloat(toHide, "alpha", 1f, 0f) 274 .setDuration(PANEL_FADE_DURATION); 275 a.addListener(new AnimatorListenerAdapter() { 276 @Override 277 public void onAnimationEnd(Animator _a) { 278 toHide.setVisibility(View.GONE); 279 if (toShow != null) { 280 if (mNotificationCount == 0) { 281 // show the frame for settings, hide for notifications 282 setContentFrameVisible(toShow == mSettingsView, true); 283 } 284 285 toShow.setVisibility(View.VISIBLE); 286 if (toShow == mSettingsView || mNotificationCount > 0) { 287 ObjectAnimator.ofFloat(toShow, "alpha", 0f, 1f) 288 .setDuration(PANEL_FADE_DURATION) 289 .start(); 290 } 291 292 if (toHide == mSettingsView) { 293 removeSettingsView(); 294 } 295 } 296 updateClearButton(); 297 updatePanelModeButtons(); 298 } 299 }); 300 a.start(); 301 } 302 updateClearButton()303 public void updateClearButton() { 304 if (mBar != null) { 305 final boolean showX 306 = (isShowing() 307 && mHasClearableNotifications 308 && mNotificationScroller.getVisibility() == View.VISIBLE); 309 getClearButton().setVisibility(showX ? View.VISIBLE : View.INVISIBLE); 310 } 311 } 312 setClearable(boolean clearable)313 public void setClearable(boolean clearable) { 314 mHasClearableNotifications = clearable; 315 } 316 updatePanelModeButtons()317 public void updatePanelModeButtons() { 318 final boolean settingsVisible = (mSettingsView != null); 319 mSettingsButton.setVisibility(!settingsVisible ? View.VISIBLE : View.INVISIBLE); 320 mNotificationButton.setVisibility(settingsVisible ? View.VISIBLE : View.INVISIBLE); 321 } 322 isInContentArea(int x, int y)323 public boolean isInContentArea(int x, int y) { 324 mContentArea.left = mTitleArea.getLeft() + mTitleArea.getPaddingLeft(); 325 mContentArea.top = mTitleArea.getTop() + mTitleArea.getPaddingTop() 326 + (int)mContentParent.getTranslationY(); // account for any adjustment 327 mContentArea.right = mTitleArea.getRight() - mTitleArea.getPaddingRight(); 328 329 View theBottom = (mContentFrame.getVisibility() == View.VISIBLE) 330 ? mContentFrame : mTitleArea; 331 mContentArea.bottom = theBottom.getBottom() - theBottom.getPaddingBottom(); 332 333 offsetDescendantRectToMyCoords(mContentParent, mContentArea); 334 return mContentArea.contains(x, y); 335 } 336 removeSettingsView()337 void removeSettingsView() { 338 if (mSettingsView != null) { 339 mContentFrame.removeView(mSettingsView); 340 mSettingsView = null; 341 } 342 } 343 344 // NB: it will be invisible until you show it addSettingsView()345 void addSettingsView() { 346 LayoutInflater infl = LayoutInflater.from(getContext()); 347 mSettingsView = infl.inflate(R.layout.status_bar_settings_view, mContentFrame, false); 348 mSettingsView.setVisibility(View.GONE); 349 mContentFrame.addView(mSettingsView); 350 } 351 352 private class Choreographer implements Animator.AnimatorListener { 353 boolean mVisible; 354 int mPanelHeight; 355 AnimatorSet mContentAnim; 356 357 // should group this into a multi-property animation 358 final static int OPEN_DURATION = 250; 359 final static int CLOSE_DURATION = 250; 360 361 // the panel will start to appear this many px from the end 362 final int HYPERSPACE_OFFRAMP = 200; 363 Choreographer()364 Choreographer() { 365 } 366 createAnimation(boolean appearing)367 void createAnimation(boolean appearing) { 368 // mVisible: previous state; appearing: new state 369 370 float start, end; 371 372 // 0: on-screen 373 // height: off-screen 374 float y = mContentParent.getTranslationY(); 375 if (appearing) { 376 // we want to go from near-the-top to the top, unless we're half-open in the right 377 // general vicinity 378 end = 0; 379 if (mNotificationCount == 0) { 380 end += mContentFrameMissingTranslation; 381 } 382 start = HYPERSPACE_OFFRAMP+end; 383 } else { 384 start = y; 385 end = y + HYPERSPACE_OFFRAMP; 386 } 387 388 Animator posAnim = ObjectAnimator.ofFloat(mContentParent, "translationY", 389 start, end); 390 posAnim.setInterpolator(appearing ? sDecelerateInterpolator : sAccelerateInterpolator); 391 392 if (mContentAnim != null && mContentAnim.isRunning()) { 393 mContentAnim.cancel(); 394 } 395 396 Animator fadeAnim = ObjectAnimator.ofFloat(mContentParent, "alpha", 397 appearing ? 1.0f : 0.0f); 398 fadeAnim.setInterpolator(appearing ? sAccelerateInterpolator : sDecelerateInterpolator); 399 400 mContentAnim = new AnimatorSet(); 401 mContentAnim 402 .play(fadeAnim) 403 .with(posAnim) 404 ; 405 mContentAnim.setDuration((DEBUG?10:1)*(appearing ? OPEN_DURATION : CLOSE_DURATION)); 406 mContentAnim.addListener(this); 407 } 408 startAnimation(boolean appearing)409 void startAnimation(boolean appearing) { 410 if (DEBUG) Slog.d(TAG, "startAnimation(appearing=" + appearing + ")"); 411 412 createAnimation(appearing); 413 mContentAnim.start(); 414 415 mVisible = appearing; 416 417 // we want to start disappearing promptly 418 if (!mVisible) updateClearButton(); 419 } 420 onAnimationCancel(Animator animation)421 public void onAnimationCancel(Animator animation) { 422 if (DEBUG) Slog.d(TAG, "onAnimationCancel"); 423 } 424 onAnimationEnd(Animator animation)425 public void onAnimationEnd(Animator animation) { 426 if (DEBUG) Slog.d(TAG, "onAnimationEnd"); 427 if (! mVisible) { 428 setVisibility(View.GONE); 429 } 430 mContentParent.setLayerType(View.LAYER_TYPE_NONE, null); 431 mContentAnim = null; 432 433 // we want to show the X lazily 434 if (mVisible) updateClearButton(); 435 } 436 onAnimationRepeat(Animator animation)437 public void onAnimationRepeat(Animator animation) { 438 } 439 onAnimationStart(Animator animation)440 public void onAnimationStart(Animator animation) { 441 } 442 } 443 } 444 445