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