/*
* 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;
}
}