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.systemui.statusbar.policy; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.ObjectAnimator; 22 import android.animation.ValueAnimator; 23 import android.content.Context; 24 import android.content.res.Configuration; 25 import android.content.res.TypedArray; 26 import android.graphics.Rect; 27 import android.util.AttributeSet; 28 import android.util.Log; 29 import android.util.Slog; 30 import android.view.MotionEvent; 31 import android.view.View; 32 import android.view.ViewConfiguration; 33 import android.view.ViewGroup; 34 35 import com.android.systemui.R; 36 import com.android.systemui.SwipeHelper; 37 38 import java.util.HashMap; 39 40 public class NotificationRowLayout extends ViewGroup implements SwipeHelper.Callback { 41 private static final String TAG = "NotificationRowLayout"; 42 private static final boolean DEBUG = false; 43 private static final boolean SLOW_ANIMATIONS = DEBUG; 44 45 private static final int APPEAR_ANIM_LEN = SLOW_ANIMATIONS ? 5000 : 250; 46 private static final int DISAPPEAR_ANIM_LEN = APPEAR_ANIM_LEN; 47 48 boolean mAnimateBounds = true; 49 50 Rect mTmpRect = new Rect(); 51 int mNumRows = 0; 52 int mRowHeight = 0; 53 int mHeight = 0; 54 55 HashMap<View, ValueAnimator> mAppearingViews = new HashMap<View, ValueAnimator>(); 56 HashMap<View, ValueAnimator> mDisappearingViews = new HashMap<View, ValueAnimator>(); 57 58 private SwipeHelper mSwipeHelper; 59 60 // Flag set during notification removal animation to avoid causing too much work until 61 // animation is done 62 boolean mRemoveViews = true; 63 64 public NotificationRowLayout(Context context, AttributeSet attrs) { 65 this(context, attrs, 0); 66 } 67 68 public NotificationRowLayout(Context context, AttributeSet attrs, int defStyle) { 69 super(context, attrs, defStyle); 70 71 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NotificationRowLayout, 72 defStyle, 0); 73 mRowHeight = a.getDimensionPixelSize(R.styleable.NotificationRowLayout_rowHeight, 0); 74 a.recycle(); 75 76 setLayoutTransition(null); 77 78 if (DEBUG) { 79 setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() { 80 @Override 81 public void onChildViewAdded(View parent, View child) { 82 Slog.d(TAG, "view added: " + child + "; new count: " + getChildCount()); 83 } 84 @Override 85 public void onChildViewRemoved(View parent, View child) { 86 Slog.d(TAG, "view removed: " + child + "; new count: " + (getChildCount() - 1)); 87 } 88 }); 89 90 setBackgroundColor(0x80FF8000); 91 } 92 93 float densityScale = getResources().getDisplayMetrics().density; 94 float pagingTouchSlop = ViewConfiguration.get(mContext).getScaledPagingTouchSlop(); 95 mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, pagingTouchSlop); 96 } 97 98 public void setAnimateBounds(boolean anim) { 99 mAnimateBounds = anim; 100 } 101 102 @Override 103 public boolean onInterceptTouchEvent(MotionEvent ev) { 104 if (DEBUG) Log.v(TAG, "onInterceptTouchEvent()"); 105 return mSwipeHelper.onInterceptTouchEvent(ev) || 106 super.onInterceptTouchEvent(ev); 107 } 108 109 @Override 110 public boolean onTouchEvent(MotionEvent ev) { 111 return mSwipeHelper.onTouchEvent(ev) || 112 super.onTouchEvent(ev); 113 } 114 115 public boolean canChildBeDismissed(View v) { 116 final View veto = v.findViewById(R.id.veto); 117 return (veto != null && veto.getVisibility() != View.GONE); 118 } 119 120 public void onChildDismissed(View v) { 121 final View veto = v.findViewById(R.id.veto); 122 if (veto != null && veto.getVisibility() != View.GONE && mRemoveViews) { 123 veto.performClick(); 124 } 125 } 126 127 public void onBeginDrag(View v) { 128 // We need to prevent the surrounding ScrollView from intercepting us now; 129 // the scroll position will be locked while we swipe 130 requestDisallowInterceptTouchEvent(true); 131 } 132 133 public void onDragCancelled(View v) { 134 } 135 136 public View getChildAtPosition(MotionEvent ev) { 137 // find the view under the pointer, accounting for GONE views 138 final int count = getChildCount(); 139 int y = 0; 140 int touchY = (int) ev.getY(); 141 int childIdx = 0; 142 View slidingChild; 143 for (; childIdx < count; childIdx++) { 144 slidingChild = getChildAt(childIdx); 145 if (slidingChild.getVisibility() == GONE) { 146 continue; 147 } 148 y += mRowHeight; 149 if (touchY < y) return slidingChild; 150 } 151 return null; 152 } 153 154 public View getChildContentView(View v) { 155 return v; 156 } 157 158 @Override 159 protected void onConfigurationChanged(Configuration newConfig) { 160 super.onConfigurationChanged(newConfig); 161 float densityScale = getResources().getDisplayMetrics().density; 162 mSwipeHelper.setDensityScale(densityScale); 163 float pagingTouchSlop = ViewConfiguration.get(mContext).getScaledPagingTouchSlop(); 164 mSwipeHelper.setPagingTouchSlop(pagingTouchSlop); 165 } 166 167 //** 168 @Override 169 public void addView(View child, int index, LayoutParams params) { 170 super.addView(child, index, params); 171 172 final View childF = child; 173 174 if (mAnimateBounds) { 175 final ObjectAnimator alphaFade = ObjectAnimator.ofFloat(child, "alpha", 0f, 1f); 176 alphaFade.setDuration(APPEAR_ANIM_LEN); 177 alphaFade.addListener(new AnimatorListenerAdapter() { 178 @Override 179 public void onAnimationEnd(Animator animation) { 180 mAppearingViews.remove(childF); 181 requestLayout(); // pick up any final changes in position 182 } 183 }); 184 185 alphaFade.start(); 186 187 mAppearingViews.put(child, alphaFade); 188 189 requestLayout(); // start the container animation 190 } 191 } 192 193 /** 194 * Sets a flag to tell us whether to actually remove views. Removal is delayed by setting this 195 * to false during some animations to smooth out performance. Callers should restore the 196 * flag to true after the animation is done, and then they should make sure that the views 197 * get removed properly. 198 */ 199 public void setViewRemoval(boolean removeViews) { 200 mRemoveViews = removeViews; 201 } 202 203 public void dismissRowAnimated(View child) { 204 dismissRowAnimated(child, 0); 205 } 206 207 public void dismissRowAnimated(View child, int vel) { 208 mSwipeHelper.dismissChild(child, vel); 209 } 210 211 @Override 212 public void removeView(View child) { 213 if (!mRemoveViews) { 214 // This flag is cleared during an animation that removes all notifications. There 215 // should be a call to remove all notifications when the animation is done, at which 216 // time the view will be removed. 217 return; 218 } 219 if (mAnimateBounds) { 220 if (mAppearingViews.containsKey(child)) { 221 mAppearingViews.remove(child); 222 } 223 224 // Don't fade it out if it already has a low alpha value, but run a non-visual 225 // animation which is used by onLayout() to animate shrinking the gap that it left 226 // in the list 227 ValueAnimator anim; 228 float currentAlpha = child.getAlpha(); 229 if (currentAlpha > .1) { 230 anim = ObjectAnimator.ofFloat(child, "alpha", currentAlpha, 0); 231 } else { 232 if (currentAlpha > 0) { 233 // Just make it go away - no need to render it anymore 234 child.setAlpha(0); 235 } 236 anim = ValueAnimator.ofFloat(0, 1); 237 } 238 anim.setDuration(DISAPPEAR_ANIM_LEN); 239 final View childF = child; 240 anim.addListener(new AnimatorListenerAdapter() { 241 @Override 242 public void onAnimationEnd(Animator animation) { 243 if (DEBUG) Slog.d(TAG, "actually removing child: " + childF); 244 NotificationRowLayout.super.removeView(childF); 245 mDisappearingViews.remove(childF); 246 requestLayout(); // pick up any final changes in position 247 } 248 }); 249 250 anim.start(); 251 mDisappearingViews.put(child, anim); 252 253 requestLayout(); // start the container animation 254 } else { 255 super.removeView(child); 256 } 257 } 258 //** 259 260 @Override 261 public void onFinishInflate() { 262 super.onFinishInflate(); 263 setWillNotDraw(false); 264 } 265 266 @Override 267 public void onDraw(android.graphics.Canvas c) { 268 super.onDraw(c); 269 if (DEBUG) { 270 //Slog.d(TAG, "onDraw: canvas height: " + c.getHeight() + "px; measured height: " 271 // + getMeasuredHeight() + "px"); 272 c.save(); 273 c.clipRect(6, 6, c.getWidth() - 6, getMeasuredHeight() - 6, 274 android.graphics.Region.Op.DIFFERENCE); 275 c.drawColor(0xFFFF8000); 276 c.restore(); 277 } 278 } 279 280 @Override 281 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 282 final int count = getChildCount(); 283 284 // pass 1: count the number of non-GONE views 285 int numRows = 0; 286 for (int i = 0; i < count; i++) { 287 final View child = getChildAt(i); 288 if (child.getVisibility() == GONE) { 289 continue; 290 } 291 if (mDisappearingViews.containsKey(child)) { 292 continue; 293 } 294 numRows++; 295 } 296 if (numRows != mNumRows) { 297 // uh oh, now you made us go and do work 298 299 final int computedHeight = numRows * mRowHeight; 300 if (DEBUG) { 301 Slog.d(TAG, String.format("rows went from %d to %d, resizing to %dpx", 302 mNumRows, numRows, computedHeight)); 303 } 304 305 mNumRows = numRows; 306 307 if (mAnimateBounds && isShown()) { 308 ObjectAnimator.ofInt(this, "forcedHeight", computedHeight) 309 .setDuration(APPEAR_ANIM_LEN) 310 .start(); 311 } else { 312 setForcedHeight(computedHeight); 313 } 314 } 315 316 // pass 2: you know, do the measuring 317 final int childWidthMS = widthMeasureSpec; 318 final int childHeightMS = MeasureSpec.makeMeasureSpec( 319 mRowHeight, MeasureSpec.EXACTLY); 320 321 for (int i = 0; i < count; i++) { 322 final View child = getChildAt(i); 323 if (child.getVisibility() == GONE) { 324 continue; 325 } 326 327 child.measure(childWidthMS, childHeightMS); 328 } 329 330 setMeasuredDimension( 331 getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), 332 resolveSize(getForcedHeight(), heightMeasureSpec)); 333 } 334 335 @Override 336 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 337 final int width = right - left; 338 final int height = bottom - top; 339 340 if (DEBUG) Slog.d(TAG, "onLayout: height=" + height); 341 342 final int count = getChildCount(); 343 int y = 0; 344 for (int i = 0; i < count; i++) { 345 final View child = getChildAt(i); 346 if (child.getVisibility() == GONE) { 347 continue; 348 } 349 float progress = 1.0f; 350 if (mDisappearingViews.containsKey(child)) { 351 progress = 1.0f - mDisappearingViews.get(child).getAnimatedFraction(); 352 } else if (mAppearingViews.containsKey(child)) { 353 progress = 1.0f - mAppearingViews.get(child).getAnimatedFraction(); 354 } 355 if (progress > 1.0f) { 356 if (DEBUG) { 357 Slog.w(TAG, "progress=" + progress + " > 1!!! " + child); 358 } 359 progress = 1f; 360 } 361 final int thisRowHeight = (int)(progress * mRowHeight); 362 if (DEBUG) { 363 Slog.d(TAG, String.format( 364 "laying out child #%d: (0, %d, %d, %d) h=%d", 365 i, y, width, y + thisRowHeight, thisRowHeight)); 366 } 367 child.layout(0, y, width, y + thisRowHeight); 368 y += thisRowHeight; 369 } 370 if (DEBUG) { 371 Slog.d(TAG, "onLayout: final y=" + y); 372 } 373 } 374 375 public void setForcedHeight(int h) { 376 if (DEBUG) Slog.d(TAG, "forcedHeight: " + h); 377 if (h != mHeight) { 378 mHeight = h; 379 requestLayout(); 380 } 381 } 382 383 public int getForcedHeight() { 384 return mHeight; 385 } 386 } 387