/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.statusbar.policy; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; import android.util.Slog; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import com.android.systemui.R; import com.android.systemui.SwipeHelper; import java.util.HashMap; public class NotificationRowLayout extends ViewGroup implements SwipeHelper.Callback { private static final String TAG = "NotificationRowLayout"; private static final boolean DEBUG = false; private static final boolean SLOW_ANIMATIONS = DEBUG; private static final int APPEAR_ANIM_LEN = SLOW_ANIMATIONS ? 5000 : 250; private static final int DISAPPEAR_ANIM_LEN = APPEAR_ANIM_LEN; boolean mAnimateBounds = true; Rect mTmpRect = new Rect(); int mNumRows = 0; int mRowHeight = 0; int mHeight = 0; HashMap<View, ValueAnimator> mAppearingViews = new HashMap<View, ValueAnimator>(); HashMap<View, ValueAnimator> mDisappearingViews = new HashMap<View, ValueAnimator>(); private SwipeHelper mSwipeHelper; // Flag set during notification removal animation to avoid causing too much work until // animation is done boolean mRemoveViews = true; public NotificationRowLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public NotificationRowLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NotificationRowLayout, defStyle, 0); mRowHeight = a.getDimensionPixelSize(R.styleable.NotificationRowLayout_rowHeight, 0); a.recycle(); setLayoutTransition(null); if (DEBUG) { setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() { @Override public void onChildViewAdded(View parent, View child) { Slog.d(TAG, "view added: " + child + "; new count: " + getChildCount()); } @Override public void onChildViewRemoved(View parent, View child) { Slog.d(TAG, "view removed: " + child + "; new count: " + (getChildCount() - 1)); } }); setBackgroundColor(0x80FF8000); } float densityScale = getResources().getDisplayMetrics().density; float pagingTouchSlop = ViewConfiguration.get(mContext).getScaledPagingTouchSlop(); mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, pagingTouchSlop); } public void setAnimateBounds(boolean anim) { mAnimateBounds = anim; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (DEBUG) Log.v(TAG, "onInterceptTouchEvent()"); return mSwipeHelper.onInterceptTouchEvent(ev) || super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev) { return mSwipeHelper.onTouchEvent(ev) || super.onTouchEvent(ev); } public boolean canChildBeDismissed(View v) { final View veto = v.findViewById(R.id.veto); return (veto != null && veto.getVisibility() != View.GONE); } public void onChildDismissed(View v) { final View veto = v.findViewById(R.id.veto); if (veto != null && veto.getVisibility() != View.GONE && mRemoveViews) { veto.performClick(); } } public void onBeginDrag(View v) { // We need to prevent the surrounding ScrollView from intercepting us now; // the scroll position will be locked while we swipe requestDisallowInterceptTouchEvent(true); } public void onDragCancelled(View v) { } public View getChildAtPosition(MotionEvent ev) { // find the view under the pointer, accounting for GONE views final int count = getChildCount(); int y = 0; int touchY = (int) ev.getY(); int childIdx = 0; View slidingChild; for (; childIdx < count; childIdx++) { slidingChild = getChildAt(childIdx); if (slidingChild.getVisibility() == GONE) { continue; } y += mRowHeight; if (touchY < y) return slidingChild; } return null; } public View getChildContentView(View v) { return v; } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); float densityScale = getResources().getDisplayMetrics().density; mSwipeHelper.setDensityScale(densityScale); float pagingTouchSlop = ViewConfiguration.get(mContext).getScaledPagingTouchSlop(); mSwipeHelper.setPagingTouchSlop(pagingTouchSlop); } //** @Override public void addView(View child, int index, LayoutParams params) { super.addView(child, index, params); final View childF = child; if (mAnimateBounds) { final ObjectAnimator alphaFade = ObjectAnimator.ofFloat(child, "alpha", 0f, 1f); alphaFade.setDuration(APPEAR_ANIM_LEN); alphaFade.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mAppearingViews.remove(childF); requestLayout(); // pick up any final changes in position } }); alphaFade.start(); mAppearingViews.put(child, alphaFade); requestLayout(); // start the container animation } } /** * Sets a flag to tell us whether to actually remove views. Removal is delayed by setting this * to false during some animations to smooth out performance. Callers should restore the * flag to true after the animation is done, and then they should make sure that the views * get removed properly. */ public void setViewRemoval(boolean removeViews) { mRemoveViews = removeViews; } public void dismissRowAnimated(View child) { dismissRowAnimated(child, 0); } public void dismissRowAnimated(View child, int vel) { mSwipeHelper.dismissChild(child, vel); } @Override public void removeView(View child) { if (!mRemoveViews) { // This flag is cleared during an animation that removes all notifications. There // should be a call to remove all notifications when the animation is done, at which // time the view will be removed. return; } if (mAnimateBounds) { if (mAppearingViews.containsKey(child)) { mAppearingViews.remove(child); } // Don't fade it out if it already has a low alpha value, but run a non-visual // animation which is used by onLayout() to animate shrinking the gap that it left // in the list ValueAnimator anim; float currentAlpha = child.getAlpha(); if (currentAlpha > .1) { anim = ObjectAnimator.ofFloat(child, "alpha", currentAlpha, 0); } else { if (currentAlpha > 0) { // Just make it go away - no need to render it anymore child.setAlpha(0); } anim = ValueAnimator.ofFloat(0, 1); } anim.setDuration(DISAPPEAR_ANIM_LEN); final View childF = child; anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (DEBUG) Slog.d(TAG, "actually removing child: " + childF); NotificationRowLayout.super.removeView(childF); mDisappearingViews.remove(childF); requestLayout(); // pick up any final changes in position } }); anim.start(); mDisappearingViews.put(child, anim); requestLayout(); // start the container animation } else { super.removeView(child); } } //** @Override public void onFinishInflate() { super.onFinishInflate(); setWillNotDraw(false); } @Override public void onDraw(android.graphics.Canvas c) { super.onDraw(c); if (DEBUG) { //Slog.d(TAG, "onDraw: canvas height: " + c.getHeight() + "px; measured height: " // + getMeasuredHeight() + "px"); c.save(); c.clipRect(6, 6, c.getWidth() - 6, getMeasuredHeight() - 6, android.graphics.Region.Op.DIFFERENCE); c.drawColor(0xFFFF8000); c.restore(); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int count = getChildCount(); // pass 1: count the number of non-GONE views int numRows = 0; for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() == GONE) { continue; } if (mDisappearingViews.containsKey(child)) { continue; } numRows++; } if (numRows != mNumRows) { // uh oh, now you made us go and do work final int computedHeight = numRows * mRowHeight; if (DEBUG) { Slog.d(TAG, String.format("rows went from %d to %d, resizing to %dpx", mNumRows, numRows, computedHeight)); } mNumRows = numRows; if (mAnimateBounds && isShown()) { ObjectAnimator.ofInt(this, "forcedHeight", computedHeight) .setDuration(APPEAR_ANIM_LEN) .start(); } else { setForcedHeight(computedHeight); } } // pass 2: you know, do the measuring final int childWidthMS = widthMeasureSpec; final int childHeightMS = MeasureSpec.makeMeasureSpec( mRowHeight, MeasureSpec.EXACTLY); for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() == GONE) { continue; } child.measure(childWidthMS, childHeightMS); } setMeasuredDimension( getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), resolveSize(getForcedHeight(), heightMeasureSpec)); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { final int width = right - left; final int height = bottom - top; if (DEBUG) Slog.d(TAG, "onLayout: height=" + height); final int count = getChildCount(); int y = 0; for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() == GONE) { continue; } float progress = 1.0f; if (mDisappearingViews.containsKey(child)) { progress = 1.0f - mDisappearingViews.get(child).getAnimatedFraction(); } else if (mAppearingViews.containsKey(child)) { progress = 1.0f - mAppearingViews.get(child).getAnimatedFraction(); } if (progress > 1.0f) { if (DEBUG) { Slog.w(TAG, "progress=" + progress + " > 1!!! " + child); } progress = 1f; } final int thisRowHeight = (int)(progress * mRowHeight); if (DEBUG) { Slog.d(TAG, String.format( "laying out child #%d: (0, %d, %d, %d) h=%d", i, y, width, y + thisRowHeight, thisRowHeight)); } child.layout(0, y, width, y + thisRowHeight); y += thisRowHeight; } if (DEBUG) { Slog.d(TAG, "onLayout: final y=" + y); } } public void setForcedHeight(int h) { if (DEBUG) Slog.d(TAG, "forcedHeight: " + h); if (h != mHeight) { mHeight = h; requestLayout(); } } public int getForcedHeight() { return mHeight; } }