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