Java tutorial
/* * Copyright (C) 2015 Zemin Liu * * 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 zemin.notification; import android.animation.Animator; import android.animation.ArgbEvaluator; import android.animation.IntEvaluator; import android.animation.ObjectAnimator; import android.content.Context; import android.content.res.Configuration; import android.graphics.Color; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.LayerDrawable; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.AttributeSet; import android.util.Log; import android.util.Property; import android.view.animation.Animation; import android.view.GestureDetector; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageSwitcher; import android.widget.ImageView; import android.widget.TextSwitcher; import android.widget.TextView; import android.widget.ViewSwitcher; import android.support.v4.util.ArrayMap; import android.support.v4.view.GestureDetectorCompat; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collection; import java.util.ListIterator; /** * Notification view. * * Callback {@link NotificationViewCallback} must be set before this view is displayed, * otherwise exception {@link CallbackNotFoundException} will be thrown. * * @see NotificationView#setCallback. * * SDK Ver. >= {@link android.os.Build.VERSION_CODES.HONEYCOMB}. */ public class NotificationView extends FrameLayout implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener { private String TAG = "zemin.NotificationView"; public static boolean DBG; public static final int NOTIFICATION_DISPLAY_TIME = 3000; public static final int BACKGROUND_TRANSITION_TIME = 1000; public static final int SHOW_TRANSITION_TIME = 500; public static final int HIDE_TRANSITION_TIME = 500; public static final float DEFAULT_CORNER_RADIUS = 8.0f; public static final int DEFAULT_BACKGROUND_COLOR = 0xfffafafa; public static final int DISMISS_FREEZE_TIME = 1200; public static final float DISMISS_GESTURE_VELOCITY = 120.0f; public static final float DISMISS_DRAG_DISTANCE_FACTOR = 0.7f; public static final int DRAG_OUT_TRANSITION_TIME = 500; public static final int DRAG_CANCEL_TRANSITION_TIME = 500; public static final int GESTURE_CONSUMER_DEFAULT = 0; public static final int GESTURE_CONSUMER_USER = 1; public static final int X = 0; public static final int Y = 1; public static final int DEFAULT_GRAVITY = Gravity.CENTER | Gravity.TOP; private final ArrayList<NotificationEntry> mEntries = new ArrayList<NotificationEntry>(); private final ArrayMap<Integer, ChildView> mChildViews = new ArrayMap<Integer, ChildView>(); private ArrayList<StateListener> mListeners = null; private final Object mEntryLock = new Object(); /** [0] w [1] h */ private final int[] mDimension = new int[2]; /** [0] l [1] t [2] r [3] b */ private final int[] mContentPadding = new int[4]; /** [0] l [1] t [2] r [3] b */ private final int[] mContentMargin = new int[4]; private Context mContext; private NotificationViewCallback mCallback; private NotificationHandler mNotificationHandler; private NotificationEntry mLastEntry; private NotificationEntry mPendingEntry; private GestureListener mGestureListener; private GestureDetectorCompat mGestureDetector; private View mContentView; private View mTargetContentView; private View mDefaultContentView; private int mDefaultLayoutId; private int mCurrentLayoutId; private int mGravity; private int mShowTransitionTime = SHOW_TRANSITION_TIME; private int mHideTransitionTime = HIDE_TRANSITION_TIME; private int mNotiDisplayTime = NOTIFICATION_DISPLAY_TIME; private ObjectAnimator mBackgroundColorAnimator; private ObjectAnimator mBackgroundAlphaAnimator; private AnimationListener mHideAnimationListener; private ContentViewSwitcher mContentViewSwitcher; private boolean mShowHideAnimEnabled = true; private Animation mDefaultShowAnimation; private Animation mDefaultHideAnimation; private Animation mShowAnimation; private Animation mHideAnimation; private Drawable mBackground; private Drawable mContentBackground; private int mStrokeWidth; private int mStrokeColor; private float mCornerRadius; private int mDefaultBackgroundColor = DEFAULT_BACKGROUND_COLOR; private int mDefaultBackgroundAlpha = 0xff; private boolean mShadowEnabled; private int mBackgroundTransitionTime = BACKGROUND_TRANSITION_TIME; private boolean mDismissOnHomeKey = false; private float mDismissOnDragDistanceFarEnough; private boolean mDismissableOnGesture = true; private boolean mDismissOnGestureEnabled; private boolean mDismissOnClick = true; private int mDirection = -1; private int mGestureConsumer; /** * Monitor the state of this view. */ public interface StateListener { /** * Called when this view starts ticking. * * @param view */ void onViewTicking(NotificationView view); /** * Called when this view is dismissed. * * @param view */ void onViewDismiss(NotificationView view); } /** * A convenience class to extend when you only want to listen for a subset * of all states. This implements all methods in the {@link StateListener}. */ public static class SimpleStateListener implements StateListener { public void onViewTicking(NotificationView view) { } public void onViewDismiss(NotificationView view) { } } public NotificationView(Context context) { super(context); } public NotificationView(Context context, AttributeSet attrs) { super(context, attrs); } void initialize(NotificationHandler handler) { mContext = getContext(); mNotificationHandler = handler; TAG += "@" + handler.toSimpleString(); } /** * Whether the callback {@link NotificationViewCallback} has been set. * * @return boolean */ public boolean hasCallback() { return mCallback != null; } /** * Set the callback. If not set, exception {@link CallbackNotFoundException} * will be thrown when starts ticking. * * @param cb */ public void setCallback(NotificationViewCallback cb) { if (mCallback != cb) { mCallback = cb; addState(CALLBACK_CHANGED); } } /** * Whether this view is enabled. * * @return boolean */ public boolean isViewEnabled() { return hasState(ENABLED); } /** * Enable/disable this view. If disabled, any notification delivered here will be ignored. * * @param enable */ public void setViewEnabled(boolean enable) { if (hasState(ENABLED) != enable) { if (DBG) Log.v(TAG, "enable - " + enable); if (enable) { addState(ENABLED); } else { clearState(ENABLED); } } } /** * Whether this view is ticking. * * @return boolean */ public boolean isTicking() { return hasState(TICKING) && !hasState(DISMISSED); } /** * Add state listener. * * @see NotificationView#StateListener * * @param l */ public void addStateListener(StateListener l) { if (mListeners == null) { mListeners = new ArrayList<StateListener>(); } if (!mListeners.contains(l)) { mListeners.add(l); } } /** * Remove state listener. * * @see NotificationView#StateListener * * @param l */ public void removeStateListener(StateListener l) { if (mListeners != null && mListeners.contains(l)) { mListeners.remove(l); } } /** * Set gesture listener. * * @see GestureListener * * @param l */ public void setGestureListener(GestureListener l) { mGestureListener = l; } /** * Whether it is paused. * * @return boolean */ public boolean isPaused() { return hasState(PAUSED); } /** * Pause. Any notification delivered here will be suspended. */ public void pause() { if (hasState(TICKING) && !hasState(PAUSED)) { if (DBG) Log.v(TAG, "pause. " + mEntries.size()); mContentView.animate().cancel(); addState(PAUSED); cancel(-1); } } /** * Resume. */ public void resume() { if (hasState(PAUSED)) { if (DBG) Log.v(TAG, "resume."); clearState(PAUSED); if (mContentView.isShown()) { schedule(MSG_SHOW, mNotiDisplayTime); return; } if (!mEntries.isEmpty()) { addState(TICKING); schedule(MSG_START, mNotiDisplayTime); } } } /** * Whether to dismiss when this view get clicked. * * @param dismiss */ public void setDismissOnClick(boolean dismiss) { mDismissOnClick = dismiss; } /** * Whether to dismiss when home key is pressed. * * @param dismiss */ public void setDismissOnHomeKey(boolean dismiss) { mDismissOnHomeKey = dismiss; } /** * Set notification display time. * * @param ms */ public void setDisplayTime(int ms) { mNotiDisplayTime = ms; } /** * Set background transition time. * * @param ms */ public void setBackgroundTransitionTime(int ms) { mBackgroundTransitionTime = ms; addState(CONTENT_BACKGROUND_CHANGED_MINOR); } /** * Set default background color. * * @param color */ public void setDefaultBackgroundColor(int color) { mDefaultBackgroundColor = color; } /** * Set default background alpha. * * @param alpha */ public void setDefaultBackgroundAlpha(int alpha) { mDefaultBackgroundAlpha = alpha; } /** * Get background. * * @return Drawable */ public Drawable getContentBackground() { return mContentBackground; } /** * Set background. * * @param b */ public void setContentBackground(Drawable b) { mContentBackground = b; addState(CONTENT_BACKGROUND_CHANGED); } /** * Enable/disable shadow background. * * @param enable */ public void setShadowEnabled(boolean enable) { mShadowEnabled = enable; addState(CONTENT_BACKGROUND_CHANGED); } /** * Set background corner radius. * * @param r */ public void setCornerRadius(float r) { mCornerRadius = r; addState(CONTENT_BACKGROUND_CHANGED); } /** * Set background stroke. */ public void setStroke(int width, int color) { mStrokeWidth = width; mStrokeColor = color; addState(CONTENT_BACKGROUND_CHANGED_MINOR); } /** * Set padding. * * @param l * @param t * @param r * @param b */ public void setContentPadding(int l, int t, int r, int b) { mContentPadding[0] = l; mContentPadding[1] = t; mContentPadding[2] = r; mContentPadding[3] = b; } /** * Set margin. * * @param l * @param t * @param r * @param b */ public void setContentMargin(int l, int t, int r, int b) { mContentMargin[0] = l; mContentMargin[1] = t; mContentMargin[2] = r; mContentMargin[3] = b; } /** * Set gravity. * * @param gravity */ public void setGravity(int gravity) { mGravity = gravity; } /** * Set dimension. * * @param width * @param height */ public void setDimension(int width, int height) { mDimension[0] = width; mDimension[1] = height; } /** * Enable/disable show/hide animation * * @param enable */ public void setShowHideAnimationEnabled(boolean enable) { mShowHideAnimEnabled = enable; } /** * Set transition time of show animation. * * @param ms */ public void setShowTransitionTime(int ms) { mShowTransitionTime = ms; } /** * Set transition time of hide animation. * * @param ms */ public void setHideTransitionTime(int ms) { mHideTransitionTime = ms; } /** * Set show animation (AnimationListener and duration will be replaced). * * @param anim */ public void setShowAnimation(Animation anim) { mShowAnimation = anim; } /** * Set hide animation (AnimationListener and duration will be replaced). * * @param anim */ public void setHideAnimation(Animation anim) { mHideAnimation = anim; } /** * Get contentView switcher. * * @see ContentViewSwitcher * * @return ContentViewSwitcher */ public ContentViewSwitcher getContentViewSwitcher() { return mContentViewSwitcher; } /** * Set child-view switcher. * * @see ChildView#ICON * @see ChildView#TITLE * @see ChildView#TEXT * @see ChildView#WHEN * * @param type * @param resId */ public void setChildViewSwitcher(int type, int resId) { ChildView child = mChildViews.get(Integer.valueOf(type)); if (child == null) { child = new ChildView(type); mChildViews.put(Integer.valueOf(type), child); } child.setViewSwitcher(resId); } /** * Set child-view. * * @see ChildView#ICON * @see ChildView#TITLE * @see ChildView#TEXT * @see ChildView#WHEN * * @param type * @param resId */ public void setChildView(int type, int resId) { ChildView child = mChildViews.get(Integer.valueOf(type)); if (child == null) { child = new ChildView(type); mChildViews.put(Integer.valueOf(type), child); } child.setView(resId); } /** * Remove child-view. * * @see ChildView#ICON * @see ChildView#TITLE * @see ChildView#TEXT * @see ChildView#WHEN * * @param type */ public void removeChildView(int type) { if (mChildViews.containsKey(Integer.valueOf(type))) mChildViews.remove(Integer.valueOf(type)); } /** * Get child-view. * * @param type * @return ChildView */ public ChildView getChildView(int type) { return mChildViews.get(Integer.valueOf(type)); } /** * Show child-view. * * @param type */ public void showChildView(int type) { ChildView child = mChildViews.get(Integer.valueOf(type)); if (child != null) child.show(); } /** * Hide child-view. * * @param type */ public void hideChildView(int type) { ChildView child = mChildViews.get(Integer.valueOf(type)); if (child != null) child.hide(); } /** * Set image drawable to child-view. Exception {@link java.lang.ClassCastException} will * be thrown, if the target child-view is not an instance of {@link android.widget.ImageView}. * * @see ChildView#setImageDrawable * * @param type * @param drawable */ public void setChildViewImageDrawable(int type, Drawable drawable) { ChildView child = mChildViews.get(Integer.valueOf(type)); if (child != null) child.setImageDrawable(drawable); } /** * Set text to child-view. Exception {@link java.lang.ClassCastException} will be * thrown, if the target child-view is not an instance of {@link android.widget.TextView}. * * @see ChildView#setText * * @param type * @param text */ public void setChildViewText(int type, CharSequence text) { ChildView child = mChildViews.get(Integer.valueOf(type)); if (child != null) child.setText(text); } private void resetChildViews() { Collection<ChildView> children = mChildViews.values(); for (ChildView child : children) { child.reset(); } } private void clearChildViews() { Collection<ChildView> children = mChildViews.values(); for (ChildView child : children) { child.clear(); } } /** * Whether the contentView has been changed. * * @return boolean */ public boolean isContentLayoutChanged() { return hasState(CONTENT_CHANGED); } /** * Get layout resource ID of the current contentView. * * @return int */ public int getCurrentLayoutId() { return mCurrentLayoutId; } /** * Get last notification {@link NotificationEntry}. * * @return NotificationEntry */ public NotificationEntry getLastNotification() { return mLastEntry; } /** * Whether the contentView has been created. * * @return boolean */ public boolean hasContentView() { return mContentView != null; } /** * Whether the contentView is currently visible. * * @return boolean */ public boolean isContentViewShown() { return mContentView.isShown(); } /** * Set visibility of the contentView. * * @param vis */ public void setContentViewVisibility(int vis) { if (DBG) Log.v(TAG, "setContentVisibility - vis=" + vis); mContentView.setVisibility(vis); } /** * Set contentView. * * @param resId */ public void setContentView(int resId) { if (mCurrentLayoutId != resId) { View view = inflate(mContext, resId, null); if (mDefaultContentView == null && resId == mCallback.getContentViewDefaultLayoutId(this)) { mDefaultContentView = view; mDefaultLayoutId = resId; } mCurrentLayoutId = resId; setContentView(view); } } void setContentView(View view) { if (mContentView == view || view == null) return; if (mContentView != null) { mContentViewSwitcher.start(view); } else { mContentBackground = new GradientDrawable(); addState(CONTENT_BACKGROUND_CHANGED); mDimension[0] = LayoutParams.MATCH_PARENT; mDimension[1] = LayoutParams.WRAP_CONTENT; mHideAnimationListener = mDismissAnimationListener; mDefaultShowAnimation = AnimationFactory.pushDownIn(); mDefaultHideAnimation = AnimationFactory.pushUpOut(); mContentViewSwitcher = new ContentViewSwitcher(); mGestureDetector = new GestureDetectorCompat(mContext, this); setContentViewInner(view); } } private void setContentViewInner(View view) { addState(CONTENT_CHANGED); removeAllViews(); clearChildViews(); mLastEntry = null; mDismissOnDragDistanceFarEnough = 0; view.setBackground(null); mContentView = view; mCallback.onContentViewChanged(this, view, mCurrentLayoutId); updateContentBackground(); } private void updateContentBackground() { Drawable background = mBackground; if (hasState(CONTENT_BACKGROUND_CHANGED)) { if (mContentBackground != null) { if (mShadowEnabled) { final Drawable[] layers = new Drawable[] { getResources().getDrawable(android.R.drawable.dialog_holo_light_frame), mContentBackground, }; background = new LayerDrawable(layers); int l, t, r, b; l = t = r = b = 0; for (int i = 0, size = layers.length; i < size; i++) { Rect rect = new Rect(); layers[i].getPadding(rect); l += rect.left; t += rect.top; r += rect.right; b += rect.bottom; } setContentPadding(l, t, r, b); } else { background = mContentBackground; Rect rect = new Rect(); background.getPadding(rect); setContentPadding(rect.left, rect.top, rect.right, rect.bottom); } if (mContentBackground instanceof GradientDrawable) { GradientDrawable b = (GradientDrawable) mContentBackground; b.setCornerRadius(mCornerRadius); b.setStroke(mStrokeWidth, mStrokeColor); if (mBackgroundColorAnimator != null) { mBackgroundColorAnimator.cancel(); mBackgroundColorAnimator = null; } ColorProperty colorProperty = new ColorProperty(); mBackgroundColorAnimator = ObjectAnimator.ofObject(b, colorProperty, new ArgbEvaluator(), 0, 0); mBackgroundColorAnimator.setDuration(mBackgroundTransitionTime); if (mBackgroundAlphaAnimator != null) { mBackgroundAlphaAnimator.cancel(); mBackgroundAlphaAnimator = null; } AlphaProperty alphaProperty = new AlphaProperty(); mBackgroundAlphaAnimator = ObjectAnimator.ofObject(b, alphaProperty, new IntEvaluator(), 0, 0); mBackgroundAlphaAnimator.setDuration(mBackgroundTransitionTime); } } clearState(CONTENT_BACKGROUND_CHANGED); clearState(CONTENT_BACKGROUND_CHANGED_MINOR); } else if (hasState(CONTENT_BACKGROUND_CHANGED_MINOR)) { if (mContentBackground instanceof GradientDrawable) { GradientDrawable b = (GradientDrawable) mContentBackground; b.setStroke(mStrokeWidth, mStrokeColor); mBackgroundColorAnimator.setDuration(mBackgroundTransitionTime); mBackgroundAlphaAnimator.setDuration(mBackgroundTransitionTime); } clearState(CONTENT_BACKGROUND_CHANGED_MINOR); } mBackground = background; mContentView.setBackground(background); } private void updateContentBackgroundColor(NotificationEntry entry) { if (entry.backgroundColor == 0) { entry.backgroundColor = mDefaultBackgroundColor; } final int lastColor = mLastEntry != null ? mLastEntry.backgroundColor : Color.WHITE; final int currColor = entry.backgroundColor; if (lastColor != currColor) { mBackgroundColorAnimator.cancel(); mBackgroundColorAnimator.setIntValues(lastColor, currColor); mBackgroundColorAnimator.start(); } } private void updateContentBackgroundAlpha(NotificationEntry entry) { if (entry.backgroundAlpha == NotificationEntry.INVALID) { entry.backgroundAlpha = mDefaultBackgroundAlpha; } final int lastAlpha = mLastEntry != null ? mLastEntry.backgroundAlpha : 0xff; final int currAlpha = entry.backgroundAlpha; if (lastAlpha != currAlpha) { mBackgroundAlphaAnimator.cancel(); mBackgroundAlphaAnimator.setIntValues(lastAlpha, currAlpha); mBackgroundAlphaAnimator.start(); } } private void refreshContentView() { refreshContentView(mContentView); } private void refreshContentView(View target) { mContentView.setBackground(null); setContentViewInner(target); schedule(MSG_START); } /** * Set the x translation of contentView. * * @param x */ public void setContentViewTranslationX(float x) { // if (DBG) Log.v(TAG, "setContentViewTranslationX - x=" + x); mContentView.setTranslationX(x); mDirection = X; } /** * Get the x translation of contentView. * * @return float */ public float getContentViewTranslationX() { return mContentView.getTranslationX(); } /** * Set the y translation of contentView. * * @param y */ public void setContentViewTranslationY(float y) { // if (DBG) Log.v(TAG, "setContentViewTranslationY - y=" + y); mContentView.setTranslationY(y); mDirection = Y; } /** * Get the y translation of contentView. * * @return float */ public float getContentViewTranslationY() { return mContentView.getTranslationY(); } /** * Set the opacity of contentView. * * @param alpha */ public void setContentViewAlpha(float alpha) { // if (DBG) Log.v(TAG, "setContentViewAlpha - alpha=" + alpha); mContentView.setAlpha(alpha); } /** * Get the opacity of contentView. * * @return float */ public float getContentViewAlpha() { return mContentView.getAlpha(); } /** * Set the x degree that contentView is rotated. * * @param x */ public void setContentViewRotationX(float x) { // if (DBG) Log.v(TAG, "setContentViewRotationX - degree=" + x); mContentView.setRotationX(x); } /** * Get the x degree that contentView is rotated. * * @return float */ public float getContentViewRotationX() { return mContentView.getRotationX(); } /** * Set the y degree that contentView is rotated. * * @param y */ public void setContentViewRotationY(float y) { // if (DBG) Log.v(TAG, "setContentViewRotationY - degree=" + y); mContentView.setRotationY(y); } /** * Get the y degree that contentView is rotated. * * @return float */ public float getContentViewRotationY() { return mContentView.getRotationY(); } /** * Set the x location of pivot point around which the contentView is rotated. * * @param x */ public void setContentViewPivotX(float x) { if (DBG) Log.v(TAG, "setContentViewPivotX - x=" + x); mContentView.setPivotY(x); } /** * Get the x location of pivot point around which the contentView is rotated. * * @return float */ public float getContentViewPivotX() { return mContentView.getPivotX(); } /** * Set the y location of pivot point around which the contentView is rotated. * * @param y */ public void setContentViewPivotY(float y) { if (DBG) Log.v(TAG, "setContentViewPivotY - y=" + y); mContentView.setPivotY(y); } /** * Get the y location of pivot point around which the contentView is rotated. * * @return float */ public float getContentViewPivotY() { return mContentView.getPivotY(); } /** * Rotate the contentView to x degree with animation. * * @param degree * @param alpha * @param listener * @param duration */ public void animateContentViewRotationX(float degree, float alpha, Animator.AnimatorListener listener, int duration) { if (DBG) Log.v(TAG, "animateContentViewRotationX - " + "degree=" + degree + ", alpha=" + alpha); mContentView.animate().cancel(); mContentView.animate().alpha(alpha).rotationX(degree).setListener(listener).setDuration(duration).start(); } /** * Rotate the contentView to y degree with animation. * * @param degree * @param alpha * @param listener * @param duration */ public void animateContentViewRotationY(float degree, float alpha, Animator.AnimatorListener listener, int duration) { if (DBG) Log.v(TAG, "animateContentViewRotationY - " + "degree=" + degree + ", alpha=" + alpha); mContentView.animate().cancel(); mContentView.animate().alpha(alpha).rotationY(degree).setListener(listener).setDuration(duration).start(); } /** * Move the contentView to x position with animation. * * @param x * @param alpha * @param listener * @param duration */ public void animateContentViewTranslationX(float x, float alpha, Animator.AnimatorListener listener, int duration) { if (DBG) Log.v(TAG, "animateContentViewTranslationX - " + "x=" + x + ", alpha=" + alpha); mContentView.animate().cancel(); mContentView.animate().alpha(alpha).translationX(x).setListener(listener).setDuration(duration).start(); } /** * Move the contentView to y position with animation. * * @param y * @param alpha * @param listener * @param duration */ public void animateContentViewTranslationY(float y, float alpha, Animator.AnimatorListener listener, int duration) { if (DBG) Log.v(TAG, "animateContentViewTranslationY - " + "y=" + y + ", alpha=" + alpha); mContentView.animate().cancel(); mContentView.animate().alpha(alpha).translationY(y).setListener(listener).setDuration(duration).start(); } /** * Dismiss this view. */ public void dismiss() { mContentViewSwitcher.start(0); } void onArrival(NotificationEntry entry) { synchronized (mEntryLock) { if (hasState(PAUSED)) { mNotificationHandler.onSendFinished(entry); return; } ListIterator<NotificationEntry> iter = mEntries.listIterator(); int index = mEntries.size(); while (iter.hasNext()) { if (entry.priority.higher(iter.next().priority)) { index = iter.nextIndex() - 1; break; } } mEntries.add(index, entry); if (!hasState(TICKING)) { addState(TICKING); schedule(MSG_START); } } } void onCancel(NotificationEntry entry) { synchronized (mEntryLock) { if (mEntries.contains(entry)) { mEntries.remove(entry); } } mNotificationHandler.onCancelFinished(entry); } void onCancelAll() { mContentViewSwitcher.start(0); mNotificationHandler.onCancelAllFinished(); } public void sendPendings() { synchronized (mEntryLock) { for (NotificationEntry entry : mEntries) { mNotificationHandler.onSendFinished(entry); } mEntries.clear(); } } private void show() { if (mContentView.getParent() == null) { addView(mContentView); } // reset mContentView.setTranslationX(0.0f); mContentView.setRotationX(0.0f); mContentView.setAlpha(1.0f); mContentView.setPadding(mContentPadding[0], mContentPadding[1], mContentPadding[2], mContentPadding[3]); if (mGravity == 0) { mGravity = DEFAULT_GRAVITY; } final LayoutParams lp = (LayoutParams) mContentView.getLayoutParams(); if (lp.leftMargin != mContentMargin[0] || lp.topMargin != mContentMargin[1] || lp.rightMargin != mContentMargin[2] || lp.bottomMargin != mContentMargin[3] || lp.width != mDimension[0] || lp.height != mDimension[1] || lp.gravity != mGravity) { lp.leftMargin = mContentMargin[0]; lp.topMargin = mContentMargin[1]; lp.rightMargin = mContentMargin[2]; lp.bottomMargin = mContentMargin[3]; lp.width = mDimension[0]; lp.height = mDimension[1]; lp.gravity = mGravity; mContentView.setLayoutParams(lp); } mContentView.setVisibility(VISIBLE); if (mShowHideAnimEnabled) { if (mShowAnimation == null) { mShowAnimation = mDefaultShowAnimation; } if (mShowTransitionTime == 0) { mShowTransitionTime = SHOW_TRANSITION_TIME; } mShowAnimation.setAnimationListener(mShowAnimationListener); mShowAnimation.setDuration(mShowTransitionTime); mContentView.startAnimation(mShowAnimation); } } private void hide() { if (mShowHideAnimEnabled) { if (mHideAnimation == null) { mHideAnimation = mDefaultHideAnimation; } if (mHideTransitionTime == 0) { mHideTransitionTime = HIDE_TRANSITION_TIME; } mHideAnimation.setAnimationListener(mHideAnimationListener); mHideAnimation.setDuration(mHideTransitionTime); mContentView.startAnimation(mHideAnimation); } else { mContentView.setVisibility(GONE); if (hasState(DISMISSING)) { onDismiss(); } } } private void onDismiss() { if (DBG) Log.v(TAG, "dismiss."); cancel(-1); clearState(TICKING); clearState(DISMISSING); addState(DISMISSED); mLastEntry = null; removeView(mContentView); onViewDismiss(); } private void onContentViewVisibilityChanged(boolean shown) { if (DBG) Log.v(TAG, "onContentViewVisibilityChanged - " + shown); if (shown == hasState(PAUSED)) { if (shown) { resume(); } else { pause(); onDismiss(); } } } private void onMsgStart() { if (hasState(PAUSED)) return; if (DBG) Log.v(TAG, "start"); if (getParent() == null) { throw new IllegalStateException("NotificationView should have a parent."); } if (mCallback == null) { throw new CallbackNotFoundException("NotificationView.setCallback() not called."); } addState(STARTING); if (hasState(DISMISSING)) { if (DBG) Log.v(TAG, "dismissing now. schedule next start."); schedule(MSG_START, (int) mHideAnimation.getDuration()); return; } NotificationEntry entry = mPendingEntry; synchronized (mEntryLock) { if (entry == null && !mEntries.isEmpty()) { entry = mEntries.get(0); } } if (entry == null) { Log.w(TAG, "no notification? quit."); schedule(MSG_DISMISS); return; } if (hasState(CALLBACK_CHANGED)) { clearState(CALLBACK_CHANGED); mCallback.onViewSetup(this); final int layoutId = mCallback.getContentViewDefaultLayoutId(this); if (mCurrentLayoutId != layoutId) { mDefaultContentView = null; setContentView(layoutId); } else { setContentViewInner(mContentView); } } // check whether a differenct contentView is being requested, // if so, change the contentView now. View newContentView = null; if (entry.layoutId > 0) { if (entry.layoutId != mCurrentLayoutId) { if (entry.layoutId == mDefaultLayoutId) { mCurrentLayoutId = mDefaultLayoutId; newContentView = mDefaultContentView; } else { mCurrentLayoutId = entry.layoutId; newContentView = inflate(mContext, entry.layoutId, null); } } } else if (mContentView != mDefaultContentView) { mCurrentLayoutId = mDefaultLayoutId; newContentView = mDefaultContentView; } if (!hasState(TICKING)) { if (DBG) Log.v(TAG, "not ticking? quit."); return; } if (newContentView != null) { setContentViewInner(newContentView); schedule(MSG_START); } else { resetChildViews(); if (hasState(DISMISSED)) { clearState(DISMISSED); onViewTicking(); } show(); } } private void onMsgShow() { if (hasState(PAUSED) || !hasState(ENABLED)) return; if (DBG) Log.v(TAG, "show"); NotificationEntry entry = mPendingEntry; mPendingEntry = null; synchronized (mEntryLock) { if (entry == null && !mEntries.isEmpty()) { entry = mEntries.remove(0); } } if (entry == null) { schedule(MSG_DISMISS); return; } if (!hasState(TICKING)) { if (DBG) Log.v(TAG, "not ticking? quit."); return; } // temporarily, view cannot be dismissed by user gesture. // this will be re-enabled after some delay. mDismissOnGestureEnabled = false; // view cannot be dismissed by user gesture. // it will always be placed on {@link NotificationBoard}, // unless explicitly call {@link NotificationDelegater#cancel()}. mDismissableOnGesture = !entry.ongoing; // check whether a different contentView is requested, // if so, switch to the new contentView and perform animation. if (entry.layoutId > 0) { if (entry.layoutId != mCurrentLayoutId) { mPendingEntry = entry; if (entry.layoutId == mDefaultLayoutId) { mCurrentLayoutId = mDefaultLayoutId; mPendingEntry = entry; setContentView(mDefaultContentView); } else { setContentView(entry.layoutId); } return; } } else if (mContentView != mDefaultContentView) { if (mDefaultContentView == null) { final int resId = mCallback.getContentViewDefaultLayoutId(this); mDefaultContentView = inflate(mContext, resId, null); mDefaultLayoutId = resId; } mCurrentLayoutId = mDefaultLayoutId; mPendingEntry = entry; setContentView(mDefaultContentView); return; } if (entry.showWhen && entry.whenFormatted == null) { entry.setWhen(null, entry.whenLong > 0L ? entry.whenLong : System.currentTimeMillis()); } if (DBG) Log.v(TAG, "onShowNotification - " + entry.ID); mCallback.onShowNotification(this, mContentView, entry, mCurrentLayoutId); updateContentBackground(); updateContentBackgroundColor(entry); updateContentBackgroundAlpha(entry); clearState(CONTENT_CHANGED); if (hasState(STARTING)) { // // waiting for the first layout of SWITCHER to be finished, // so that we can adjust its size according to its content. // // 100 ms // schedule(MSG_CHILD_VIEW_ADJUST_HEIGHT, 100); clearState(STARTING); } mLastEntry = entry; mNotificationHandler.onSendFinished(entry); if (!isScheduled(MSG_SWITCH_TO_SELF)) { schedule(MSG_SHOW, mNotiDisplayTime); } schedule(MSG_ENABLE_DISMISS_ON_GESTURE, DISMISS_FREEZE_TIME); } private void onMsgDismiss() { if (hasState(TICKING) && !hasState(DISMISSING)) { if (mContentView.isShown()) { addState(DISMISSING); mHideAnimationListener = mDismissAnimationListener; hide(); } else { onDismiss(); } } } private void onMsgClearAnimation() { if (mContentView.getAnimation() != null) { if (DBG) Log.v(TAG, "clearAnimation"); mContentView.clearAnimation(); } } private void onMsgSwitchToSelf() { if (!mEntries.isEmpty()) { if (DBG) Log.v(TAG, "switchToSelf"); mHideAnimationListener = mSwitchSelfAnimationListener; hide(); } else { schedule(MSG_DISMISS); } } private void onMsgSwitchToTarget(View target) { if (DBG) Log.v(TAG, "switchToTarget"); mTargetContentView = target; mHideAnimationListener = mSwitchContentAnimationListener; hide(); } private void onMsgChildViewAdjustHeight() { if (DBG) Log.v(TAG, "childViewAdjustHeight"); Collection<ChildView> children = mChildViews.values(); for (ChildView child : children) { child.adjustHeight(); } } private void onMsgEnableDismissOnGesture() { if (DBG) Log.v(TAG, "enableDismissOnGesture"); mDismissOnGestureEnabled = true; } public void onBackKey() { if (isTicking()) { schedule(MSG_DISMISS); } } public void onHomeKey() { if (isTicking() && mDismissOnHomeKey) { schedule(MSG_DISMISS); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override protected void onWindowVisibilityChanged(int visibility) { super.onWindowVisibilityChanged(visibility); if (DBG) Log.v(TAG, "onWindowVisibilityChanged: " + visibility); if (!hasState(STARTING)) { onContentViewVisibilityChanged(visibility == VISIBLE); } } @Override protected void onVisibilityChanged(View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); if (DBG) Log.v(TAG, "onVisibilityChanged: " + visibility + ", " + changedView); if (changedView == this) { onContentViewVisibilityChanged(visibility == VISIBLE); } } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); if (DBG) Log.v(TAG, "onConfigurationChanged: " + newConfig); } @Override public boolean onTouchEvent(MotionEvent event) { if (!hasState(ENABLED)) { return false; } boolean handled = mGestureDetector.onTouchEvent(event); switch (event.getAction()) { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: onUpOrCancel(event, handled); break; } return handled ? handled : super.onTouchEvent(event); } @Override public boolean onDown(MotionEvent event) { if (DBG) Log.v(TAG, "onDown"); cancel(MSG_SHOW); mContentViewSwitcher.cancelPendings(); mDirection = -1; mGestureConsumer = GESTURE_CONSUMER_DEFAULT; if (mGestureListener != null) { mGestureListener.onDown(event); } return true; } @Override public void onShowPress(MotionEvent event) { if (DBG) Log.v(TAG, "onShowPress"); if (mGestureListener != null) { mGestureListener.onShowPress(event); } } @Override public boolean onSingleTapUp(MotionEvent event) { if (DBG) Log.v(TAG, "onSingleTapUp"); boolean handled = false; if (mGestureListener != null) { handled = mGestureListener.onSingleTapUp(event); } return handled; } @Override public boolean onSingleTapConfirmed(MotionEvent event) { if (DBG) Log.v(TAG, "onSingleTapConfirmed"); boolean handled = false; if (mGestureListener != null) { handled = mGestureListener.onSingleTapConfirmed(event); } if (mDismissOnClick) { mContentViewSwitcher.start(0); } else { schedule(MSG_SHOW, mNotiDisplayTime); } mCallback.onClickContentView(this, mContentView, mLastEntry); return handled; } @Override public boolean onDoubleTap(MotionEvent event) { if (DBG) Log.v(TAG, "onDoubleTap"); boolean handled = false; if (mGestureListener != null) { handled = mGestureListener.onDoubleTap(event); } schedule(MSG_SHOW, mNotiDisplayTime); return handled; } @Override public boolean onDoubleTapEvent(MotionEvent event) { if (DBG) Log.v(TAG, "onDoubleTapEvent"); boolean handled = false; if (mGestureListener != null) { handled = mGestureListener.onDoubleTapEvent(event); } return handled; } @Override public void onLongPress(MotionEvent event) { if (DBG) Log.v(TAG, "onLongPress"); if (mGestureListener != null) { mGestureListener.onLongPress(event); } schedule(MSG_SHOW, mNotiDisplayTime); } @Override public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX, float distanceY) { // if (DBG) Log.v(TAG, "onScroll"); if (mGestureConsumer == GESTURE_CONSUMER_DEFAULT) { if (mDirection == -1 && mGestureListener != null && mGestureListener.onScroll(event1, event2, distanceX, distanceY)) { mGestureConsumer = GESTURE_CONSUMER_USER; return true; } } else if (mGestureConsumer == GESTURE_CONSUMER_USER) { return mGestureListener != null ? mGestureListener.onScroll(event1, event2, distanceX, distanceY) : false; } final int direction = Math.abs(distanceX) > Math.abs(distanceY) ? X : Y; if (mDirection != -1 && mDirection != direction) { // if (DBG) Log.v(TAG, "wrong direction(curr=" + direction + // ", prev=" + mDirection + "): skip scroll."); return false; } if (direction == X) { if (mDismissOnDragDistanceFarEnough == 0) { mDismissOnDragDistanceFarEnough = DISMISS_DRAG_DISTANCE_FACTOR * mContentView.getMeasuredWidth(); } final float x = mContentView.getTranslationX() - distanceX; float alpha = Utils.getAlphaForOffset(1.0f, 0.0f, 0.0f, mDismissOnDragDistanceFarEnough, Math.abs(x)); if (alpha < 0.0f) { alpha = 0.0f; } setContentViewTranslationX(x); setContentViewAlpha(alpha); return true; } return false; } @Override public boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) { if (DBG) Log.v(TAG, "onFling"); if (mGestureConsumer == GESTURE_CONSUMER_DEFAULT) { if (mDirection == -1 && mGestureListener != null && mGestureListener.onFling(event1, event2, velocityX, velocityY)) { return true; } } else if (mGestureConsumer == GESTURE_CONSUMER_USER) { return mGestureListener != null ? mGestureListener.onFling(event1, event2, velocityX, velocityY) : false; } final int direction = Math.abs(velocityX) > Math.abs(velocityY) ? X : Y; if (mDirection != -1 && mDirection != direction) { if (DBG) Log.v(TAG, "wrong direction(curr=" + direction + ", prev=" + mDirection + "): skip fling."); return false; } if (direction == X) { final boolean dismiss = mDismissOnGestureEnabled && mDismissableOnGesture && Math.abs(velocityX) > DISMISS_GESTURE_VELOCITY && (mContentView.getTranslationX() == 0 || mContentView.getTranslationX() > 0 == velocityX > 0); if (dismiss) { onDragOut(); } else { onDragCancel(); } return true; } else { if (velocityY < 0 && Math.abs(velocityY) > DISMISS_GESTURE_VELOCITY) { if (mDismissOnGestureEnabled) { mContentViewSwitcher.start(0); return true; } } } return false; } public void onUpOrCancel(MotionEvent event, boolean handled) { if (DBG) Log.v(TAG, "onUpOrCancel: " + handled); if (mGestureListener != null) { mGestureListener.onUpOrCancel(event, handled); } if (handled) { return; } final float x = mContentView.getTranslationX(); if (x == 0) { schedule(MSG_SHOW, mNotiDisplayTime); return; } if (mDismissOnDragDistanceFarEnough == 0) { mDismissOnDragDistanceFarEnough = DISMISS_DRAG_DISTANCE_FACTOR * mContentView.getMeasuredWidth(); } final boolean dismiss = mDismissOnGestureEnabled && mDismissableOnGesture && Math.abs(x) > mDismissOnDragDistanceFarEnough; if (dismiss) { onDragOut(); } else { onDragCancel(); } } private void onDragOut() { if (hasState(DISMISSED)) { onDragCancel(); return; } if (DBG) Log.v(TAG, "onDragOut"); final int width = mContentView.getMeasuredWidth(); final int x = mContentView.getTranslationX() >= 0 ? width : -width; animateContentViewTranslationX(x, 0.0f, mDragOutAnimatorListener, DRAG_OUT_TRANSITION_TIME); } private void onDragCancel() { if (DBG) Log.v(TAG, "onDragCancel"); animateContentViewTranslationX(0.0f, 1.0f, mDragCancelAnimatorListener, DRAG_CANCEL_TRANSITION_TIME); } public class ContentViewSwitcher { /** * switch to self */ public void start() { start(mNotiDisplayTime); } public void start(int delay) { addState(CONTENT_CHANGED); schedule(MSG_SWITCH_TO_SELF, delay); } /** * switch to target */ private void start(View target) { addState(CONTENT_CHANGED); schedule(MSG_SWITCH_TO_TARGET, 0, 0, target, 0); } /** * cancel */ public void cancelPendings() { cancel(MSG_SWITCH_TO_SELF); } } public class ChildView { public static final int ICON = 0; public static final int TITLE = 1; public static final int TEXT = 2; public static final int WHEN = 3; // add more... public static final int TRANSITION_TIME = 700; public final int type; public View view; public ViewSwitcher viewSwitcher; private Animation mInAnimation; private Animation mOutAnimation; private int mInDuration = TRANSITION_TIME; private int mOutDuration = TRANSITION_TIME; private ChildView(int type) { this.type = type; } public Animation getInAnimation() { return mInAnimation; } public Animation getOutAnimation() { return mOutAnimation; } public int getInDuration() { return mInDuration; } public int getOutDuration() { return mOutDuration; } public void setInAnimation(Animation animation, int duration) { mInAnimation = animation; mInDuration = duration; updateAnimation(); } public void setOutAnimation(Animation animation, int duration) { mOutAnimation = animation; mOutDuration = duration; updateAnimation(); } public void show() { if (viewSwitcher != null) { viewSwitcher.setVisibility(VISIBLE); } else if (view != null) { view.setVisibility(VISIBLE); } } public void hide() { if (viewSwitcher != null) { viewSwitcher.setVisibility(INVISIBLE); } else if (view != null) { view.setVisibility(INVISIBLE); } } // throws ClassCastException public void setImageDrawable(Drawable drawable) { if (viewSwitcher != null) { ((ImageSwitcher) viewSwitcher).setImageDrawable(drawable); } else if (view != null) { ((ImageView) view).setImageDrawable(drawable); } } // throws ClassCastException public void setText(CharSequence text) { if (viewSwitcher != null) { ((TextSwitcher) viewSwitcher).setText(text); } else if (view != null) { ((TextView) view).setText(text); } } private void reset() { if (viewSwitcher != null) { viewSwitcher.setAnimateFirstView(false); viewSwitcher.reset(); } } private void clear() { viewSwitcher = null; view = null; } private void setViewSwitcher(int resId) { if (DBG) Log.v(TAG, "switcher[" + type + "] update."); viewSwitcher = (ViewSwitcher) mContentView.findViewById(resId); if (viewSwitcher == null) { throw new IllegalStateException("child-view switcher not found."); } updateAnimation(); } private void setView(int resId) { if (DBG) Log.v(TAG, "view[" + type + "] update."); view = mContentView.findViewById(resId); if (view == null) { throw new IllegalStateException("child-view not found."); } } private void updateAnimation() { if (viewSwitcher == null) return; if (viewSwitcher.getInAnimation() != mInAnimation || mInAnimation == null) { if (mInAnimation == null) { mInAnimation = AnimationFactory.pushDownIn(); } mInAnimation.setAnimationListener(mInAnimationListener); mInAnimation.setDuration(mInDuration); viewSwitcher.setInAnimation(mInAnimation); } if (viewSwitcher.getOutAnimation() != mOutAnimation || mOutAnimation == null) { if (mOutAnimation == null) { mOutAnimation = AnimationFactory.pushDownOut(); } mOutAnimation.setAnimationListener(mOutAnimationListener); mOutAnimation.setDuration(mOutDuration); viewSwitcher.setOutAnimation(mOutAnimation); } } // TODO: a better way to wrap content of mContentView, especially TextView. private void adjustHeight() { if (viewSwitcher == null) return; if (type == TEXT) { TextView curr = (TextView) viewSwitcher.getCurrentView(); TextView next = (TextView) viewSwitcher.getNextView(); int currH = curr.getLineCount() * curr.getLineHeight(); int nextH = next.getLineCount() * next.getLineHeight(); if (currH != nextH) { curr.setHeight(currH); next.setHeight(currH); } } } private final AnimationListener mInAnimationListener = new AnimationListener() { @Override public void onAnimationStart(Animation animation) { adjustHeight(); } @Override public void onAnimationEnd(Animation animation) { } }; private final AnimationListener mOutAnimationListener = new AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { } }; } private class ColorProperty extends Property<GradientDrawable, Integer> { ColorProperty() { super(Integer.class, "color"); } @Override public void set(GradientDrawable gd, Integer value) { gd.setColor(value.intValue()); } @Override public Integer get(GradientDrawable gd) { return 0; } } private class AlphaProperty extends Property<GradientDrawable, Integer> { AlphaProperty() { super(Integer.class, "alpha"); } @Override public void set(GradientDrawable gd, Integer value) { gd.setAlpha(value.intValue()); } @Override public Integer get(GradientDrawable gd) { return 0; } } // show private final AnimationListener mShowAnimationListener = new AnimationListener() { @Override public void onAnimationStart(Animation animation) { if (DBG) Log.v(TAG, "show start"); schedule(MSG_CLEAR_ANIMATION, (int) animation.getDuration()); schedule(MSG_SHOW); } @Override public void onAnimationEnd(Animation animation) { if (DBG) Log.v(TAG, "show end"); animation.setAnimationListener(null); } }; // dismiss private final AnimationListener mDismissAnimationListener = new AnimationListener() { @Override public void onAnimationStart(Animation animation) { if (DBG) Log.v(TAG, "hide start"); schedule(MSG_CLEAR_ANIMATION, (int) animation.getDuration()); } @Override public void onAnimationEnd(Animation animation) { if (DBG) Log.v(TAG, "hide end"); animation.setAnimationListener(null); onDismiss(); } }; // switch to target private final AnimationListener mSwitchContentAnimationListener = new AnimationListener() { @Override public void onAnimationStart(Animation animation) { if (DBG) Log.v(TAG, "switch content start"); schedule(MSG_CLEAR_ANIMATION, (int) animation.getDuration()); } @Override public void onAnimationEnd(Animation animation) { if (DBG) Log.v(TAG, "switch content end"); animation.setAnimationListener(null); refreshContentView(mTargetContentView); mTargetContentView = null; } }; // self switch private final AnimationListener mSwitchSelfAnimationListener = new AnimationListener() { @Override public void onAnimationStart(Animation animation) { if (DBG) Log.v(TAG, "switch self start"); schedule(MSG_CLEAR_ANIMATION, (int) animation.getDuration()); } @Override public void onAnimationEnd(Animation animation) { if (DBG) Log.v(TAG, "switch self end"); animation.setAnimationListener(null); refreshContentView(); } }; // drag left/right private final AnimatorListener mDragOutAnimatorListener = new AnimatorListener() { private boolean mCanceled; @Override public void onAnimationStart(Animator animation) { if (DBG) Log.v(TAG, "drag out start"); mCanceled = false; addState(DISMISSING); } @Override public void onAnimationEnd(Animator animation) { if (!mCanceled) { if (DBG) Log.v(TAG, "drag out end"); mContentView.animate().setListener(null); mNotificationHandler.reportCanceled(mLastEntry); clearState(DISMISSING); cancel(MSG_START); refreshContentView(); } } @Override public void onAnimationCancel(Animator animation) { if (DBG) Log.v(TAG, "drag out cancel"); mCanceled = true; clearState(DISMISSING); } }; // drag cancel private final AnimatorListener mDragCancelAnimatorListener = new AnimatorListener() { @Override public void onAnimationStart(Animator animation) { if (DBG) Log.v(TAG, "drag cancel start"); } @Override public void onAnimationEnd(Animator animation) { if (DBG) Log.v(TAG, "drag cancel end"); mContentView.animate().setListener(null); schedule(MSG_SHOW, mNotiDisplayTime); } }; private void onViewTicking() { if (mListeners != null) { for (StateListener l : mListeners) { l.onViewTicking(this); } } } private void onViewDismiss() { if (mListeners != null) { for (StateListener l : mListeners) { l.onViewDismiss(this); } } } private static final int ENABLED = 0x00000001; private static final int PAUSED = 0x00000002; private static final int TICKING = 0x00000004; private static final int STARTING = 0x00000008; private static final int DISMISSING = 0x00000010; private static final int DISMISSED = 0x00000020; private static final int CONTENT_CHANGED = 0x00000100; private static final int CONTENT_BACKGROUND_CHANGED = 0x00000200; private static final int CONTENT_BACKGROUND_CHANGED_MINOR = 0x00000400; private static final int CALLBACK_CHANGED = 0x00000800; private int mState = ENABLED | DISMISSED; private void addState(int state) { mState |= state; } private void clearState(int state) { mState &= ~state; } private boolean hasState(int state) { return (mState & state) != 0; } private static final int MSG_START = 0; private static final int MSG_SHOW = 1; private static final int MSG_SWITCH_TO_SELF = 2; private static final int MSG_SWITCH_TO_TARGET = 3; private static final int MSG_DISMISS = 4; private static final int MSG_CLEAR_ANIMATION = 5; private static final int MSG_CHILD_VIEW_ADJUST_HEIGHT = 6; private static final int MSG_ENABLE_DISMISS_ON_GESTURE = 7; private H mH; private H getH() { if (mH == null) mH = new H(this); return mH; } private boolean isScheduled(int what) { final H h = getH(); return h.hasMessages(what); } private void cancel(int what) { final H h = getH(); if (what == -1) h.removeCallbacksAndMessages(null); else h.removeMessages(what); } private void schedule(int what) { final H h = getH(); h.removeMessages(what); h.sendEmptyMessage(what); } private void schedule(int what, int delay) { final H h = getH(); h.removeMessages(what); h.sendEmptyMessageDelayed(what, delay); } private void schedule(int what, int arg1, int arg2, Object obj, int delay) { final H h = getH(); h.removeMessages(what); h.sendMessageDelayed(h.obtainMessage(what, arg1, arg2, obj), delay); } // main looper private static final class H extends Handler { private WeakReference<NotificationView> mView; H(NotificationView v) { super(Looper.getMainLooper()); mView = new WeakReference<NotificationView>(v); } @Override public void handleMessage(Message msg) { NotificationView v = mView.get(); if (v == null) return; switch (msg.what) { case MSG_START: v.onMsgStart(); break; case MSG_SHOW: v.onMsgShow(); break; case MSG_SWITCH_TO_SELF: v.onMsgSwitchToSelf(); break; case MSG_SWITCH_TO_TARGET: v.onMsgSwitchToTarget((View) msg.obj); break; case MSG_DISMISS: v.onMsgDismiss(); break; case MSG_CLEAR_ANIMATION: v.onMsgClearAnimation(); break; case MSG_CHILD_VIEW_ADJUST_HEIGHT: v.onMsgChildViewAdjustHeight(); break; case MSG_ENABLE_DISMISS_ON_GESTURE: v.onMsgEnableDismissOnGesture(); break; } } } }