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