/* * Copyright (C) 2010 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 org.exalm.tabletkat.statusbar.tablet; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.ScrollView; import org.exalm.tabletkat.SystemR; import org.exalm.tabletkat.TabletKatModule; import org.exalm.tabletkat.TkR; import org.exalm.tabletkat.ViewHelper; import de.robv.android.xposed.XposedHelpers; public class NotificationPanel extends RelativeLayout implements StatusBarPanel, View.OnClickListener { private Object mExpandHelper; private LinearLayout latestItems; static final String TAG = "Tablet/NotificationPanel"; static final boolean DEBUG = false; final static int PANEL_FADE_DURATION = 150; boolean mShowing; boolean mHasClearableNotifications = false; int mNotificationCount = 0; NotificationPanelTitle mTitleArea; ImageView mSettingsButton; ImageView mNotificationButton; ScrollView mNotificationScroller; ViewGroup mContentFrame; Rect mContentArea = new Rect(); SettingsView mSettingsView; ViewGroup mContentParent; TabletStatusBarMod mBar; View mClearButton; static Interpolator sAccelerateInterpolator = new AccelerateInterpolator(); static Interpolator sDecelerateInterpolator = new DecelerateInterpolator(); // amount to slide mContentParent down by when mContentFrame is missing float mContentFrameMissingTranslation; Choreographer mChoreo = new Choreographer(); public NotificationPanel(Context context, AttributeSet attrs) { this(context, attrs, 0); } public NotificationPanel(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public void setBar(TabletStatusBarMod b) { mBar = b; } @Override public void onFinishInflate() { super.onFinishInflate(); setWillNotDraw(false); mContentParent = (ViewGroup)findViewById(TkR.id.content_parent); mContentParent.bringToFront(); mTitleArea = (NotificationPanelTitle) findViewById(TkR.id.title_area); mTitleArea.onFinishInflate(); mTitleArea.setPanel(this); mSettingsButton = (ImageView) findViewById(SystemR.id.settings_button); mNotificationButton = (ImageView) findViewById(SystemR.id.notification_button); mNotificationScroller = (ScrollView) findViewById(TkR.id.notification_scroller); mContentFrame = (ViewGroup)findViewById(TkR.id.content_frame); mContentFrameMissingTranslation = 0; // not needed with current assets // the "X" that appears in place of the clock when the panel is showing notifications mClearButton = findViewById(SystemR.id.clear_all_button); mClearButton.setOnClickListener(mClearButtonListener); mShowing = false; LinearLayout l = (LinearLayout) XposedHelpers.newInstance(TabletKatModule.mNotificationRowLayoutClass, getContext(), null); FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; l.setLayoutParams(lp); l.setId(SystemR.id.content); l.setClickable(true); l.setFocusable(true); l.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); mNotificationScroller.addView(l); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); latestItems = (LinearLayout) findViewById(SystemR.id.content); int minHeight = getResources().getDimensionPixelSize(SystemR.dimen.notification_row_min_height); int maxHeight = getResources().getDimensionPixelSize(SystemR.dimen.notification_row_max_height); mExpandHelper = XposedHelpers.newInstance(TabletKatModule.mExpandHelperClass, new Class<?>[]{Context.class, TabletKatModule.mExpandHelperCallbackClass, Integer.TYPE, Integer.TYPE}, new Object[]{getContext(), latestItems, minHeight, maxHeight}); XposedHelpers.callMethod(mExpandHelper, "setEventSource", this); XposedHelpers.callMethod(mExpandHelper, "setScrollView", mNotificationScroller); XposedHelpers.setIntField(mExpandHelper, "mGravity", Gravity.BOTTOM); } private View.OnClickListener mClearButtonListener = new View.OnClickListener() { public void onClick(View v) { mBar.clearAll(); } }; public View getClearButton() { return mClearButton; } public void show(boolean show, boolean animate) { if (animate) { if (mShowing != show) { mShowing = show; if (show) { setVisibility(View.VISIBLE); // Don't start the animation until we've created the layer, which is done // right before we are drawn mContentParent.setLayerType(View.LAYER_TYPE_HARDWARE, null); getViewTreeObserver().addOnPreDrawListener(mPreDrawListener); } else { mChoreo.startAnimation(show); } } } else { mShowing = show; setVisibility(show ? View.VISIBLE : View.GONE); } } /** * This is used only when we've created a hardware layer and are waiting until it's * been created in order to start the appearing animation. */ private ViewTreeObserver.OnPreDrawListener mPreDrawListener = new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { getViewTreeObserver().removeOnPreDrawListener(this); mChoreo.startAnimation(true); return false; } }; /** * Whether the panel is showing, or, if it's animating, whether it will be * when the animation is done. */ public boolean isShowing() { return mShowing; } @Override public void onVisibilityChanged(View v, int vis) { super.onVisibilityChanged(v, vis); // when we hide, put back the notifications if (vis != View.VISIBLE) { if (mSettingsView != null) removeSettingsView(); mNotificationScroller.setVisibility(View.VISIBLE); mNotificationScroller.setAlpha(1f); mNotificationScroller.scrollTo(0, 0); updatePanelModeButtons(); } } @Override public boolean dispatchHoverEvent(MotionEvent event) { // Ignore hover events outside of this panel bounds since such events // generate spurious accessibility events with the panel content when // tapping outside of it, thus confusing the user. final int x = (int) event.getX(); final int y = (int) event.getY(); if (x >= 0 && x < getWidth() && y >= 0 && y < getHeight()) { return super.dispatchHoverEvent(event); } return true; } @Override public boolean dispatchKeyEvent(KeyEvent event) { final int keyCode = event.getKeyCode(); switch (keyCode) { // We exclusively handle the back key by hiding this panel. case KeyEvent.KEYCODE_BACK: { if (event.getAction() == KeyEvent.ACTION_UP) { mBar.animateCollapsePanels(); } return true; } // We react to the home key but let the system handle it. case KeyEvent.KEYCODE_HOME: { if (event.getAction() == KeyEvent.ACTION_UP) { mBar.animateCollapsePanels(); } } break; } return super.dispatchKeyEvent(event); } /* @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if (DEBUG) Log.d(TAG, String.format("PANEL: onLayout: (%d, %d, %d, %d)", l, t, r, b)); } @Override public void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (DEBUG) { Log.d(TAG, String.format("PANEL: onSizeChanged: (%d -> %d, %d -> %d)", oldw, w, oldh, h)); } } */ public void onClick(View v) { if (mSettingsButton.isEnabled() && v == mTitleArea) { swapPanels(); } } public void setNotificationCount(int n) { mNotificationCount = n; } public void setContentFrameVisible(final boolean showing, boolean animate) { } public void swapPanels() { final View toShow, toHide; if (mSettingsView == null) { addSettingsView(); toShow = mSettingsView; toHide = mNotificationScroller; } else { toShow = mNotificationScroller; toHide = mSettingsView; } Animator a = ObjectAnimator.ofFloat(toHide, "alpha", 1f, 0f) .setDuration(PANEL_FADE_DURATION); a.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator _a) { toHide.setVisibility(View.GONE); if (toShow != null) { toShow.setVisibility(View.VISIBLE); if (toShow == mSettingsView || mNotificationCount > 0) { ObjectAnimator.ofFloat(toShow, "alpha", 0f, 1f) .setDuration(PANEL_FADE_DURATION) .start(); } if (toHide == mSettingsView) { removeSettingsView(); } } updateClearButton(); updatePanelModeButtons(); } }); a.start(); } public void updateClearButton() { if (mBar != null) { final boolean showX = (isShowing() && mHasClearableNotifications && mNotificationScroller.getVisibility() == View.VISIBLE); getClearButton().setVisibility(showX ? View.VISIBLE : View.INVISIBLE); } } public void setClearable(boolean clearable) { mHasClearableNotifications = clearable; } public void updatePanelModeButtons() { final boolean settingsVisible = (mSettingsView != null); mSettingsButton.setVisibility(!settingsVisible && mSettingsButton.isEnabled() ? View.VISIBLE : View.GONE); mNotificationButton.setVisibility(settingsVisible ? View.VISIBLE : View.GONE); } public boolean isInContentArea(int x, int y) { mContentArea.left = mContentFrame.getLeft() + mContentFrame.getPaddingLeft(); mContentArea.top = mContentFrame.getTop() + mContentFrame.getPaddingTop() + (int)mContentParent.getTranslationY(); // account for any adjustment mContentArea.right = mContentFrame.getRight() - mContentFrame.getPaddingRight(); mContentArea.bottom = mContentFrame.getBottom() - mContentFrame.getPaddingBottom(); offsetDescendantRectToMyCoords(mContentParent, mContentArea); return mContentArea.contains(x, y); } void removeSettingsView() { if (mSettingsView != null) { mContentFrame.removeView(mSettingsView); mSettingsView = null; } } // NB: it will be invisible until you show it void addSettingsView() { LayoutInflater infl = LayoutInflater.from(getContext()); LinearLayout l = (LinearLayout)infl.inflate(TkR.layout.system_bar_settings_view, mContentFrame, false); mSettingsView = (SettingsView) ViewHelper.replaceView(l, new SettingsView(getContext(), null)); mSettingsView.setVisibility(View.GONE); mContentFrame.addView(mSettingsView); } public void setQuickSettingsEnabled(boolean enable) { if (!enable && mSettingsView != null) { swapPanels(); } mSettingsButton.setEnabled(enable); } private class Choreographer implements Animator.AnimatorListener { boolean mVisible; int mPanelHeight; AnimatorSet mContentAnim; // should group this into a multi-property animation final static int OPEN_DURATION = 250; final static int CLOSE_DURATION = 250; // the panel will start to appear this many px from the end final int HYPERSPACE_OFFRAMP = 200; Choreographer() { } void createAnimation(boolean appearing) { // mVisible: previous state; appearing: new state float start, end; // 0: on-screen // height: off-screen float y = mContentParent.getTranslationY(); if (appearing) { // we want to go from near-the-top to the top, unless we're half-open in the right // general vicinity end = 0; if (mNotificationCount == 0) { end += mContentFrameMissingTranslation; } start = HYPERSPACE_OFFRAMP+end; } else { start = y; end = y + HYPERSPACE_OFFRAMP; } Animator posAnim = ObjectAnimator.ofFloat(mContentParent, "translationY", start, end); posAnim.setInterpolator(appearing ? sDecelerateInterpolator : sAccelerateInterpolator); if (mContentAnim != null && mContentAnim.isRunning()) { mContentAnim.cancel(); } Animator fadeAnim = ObjectAnimator.ofFloat(mContentParent, "alpha", appearing ? 1.0f : 0.0f); fadeAnim.setInterpolator(appearing ? sAccelerateInterpolator : sDecelerateInterpolator); mContentAnim = new AnimatorSet(); if (mBar.mHeadsUpNotificationView != null) { Animator headsUpFadeAnim = ObjectAnimator.ofFloat(mBar.mHeadsUpNotificationView, "alpha", appearing ? 0.0f : 1.0f); headsUpFadeAnim.setInterpolator(appearing ? sDecelerateInterpolator : sAccelerateInterpolator); mContentAnim .play(fadeAnim) .with(posAnim) .with(headsUpFadeAnim) ; } else { mContentAnim .play(fadeAnim) .with(posAnim) ; } mContentAnim.setDuration((DEBUG?10:1)*(appearing ? OPEN_DURATION : CLOSE_DURATION)); mContentAnim.addListener(this); } void startAnimation(boolean appearing) { if (DEBUG) Log.d(TAG, "startAnimation(appearing=" + appearing + ")"); createAnimation(appearing); mContentAnim.start(); mVisible = appearing; // we want to start disappearing promptly if (!mVisible) updateClearButton(); } public void onAnimationCancel(Animator animation) { if (DEBUG) Log.d(TAG, "onAnimationCancel"); } public void onAnimationEnd(Animator animation) { if (DEBUG) Log.d(TAG, "onAnimationEnd"); if (! mVisible) { setVisibility(View.GONE); } mContentParent.setLayerType(View.LAYER_TYPE_NONE, null); mContentAnim = null; // we want to show the X lazily if (mVisible) updateClearButton(); } public void onAnimationRepeat(Animator animation) { } public void onAnimationStart(Animator animation) { } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (mSettingsView != null) { return super.onInterceptTouchEvent(ev); } MotionEvent cancellation = MotionEvent.obtain(ev); cancellation.setAction(MotionEvent.ACTION_CANCEL); boolean intercept = (Boolean)XposedHelpers.callMethod(mExpandHelper, "onInterceptTouchEvent", ev) || super.onInterceptTouchEvent(ev); if (intercept) { XposedHelpers.callMethod(latestItems, "onInterceptTouchEvent", cancellation); } return intercept; } @Override public boolean onTouchEvent(MotionEvent ev) { if (mSettingsView != null) { return super.onTouchEvent(ev); } boolean handled = (Boolean)XposedHelpers.callMethod(mExpandHelper, "onTouchEvent", ev) || super.onTouchEvent(ev); return handled; } public void setSettingsEnabled(boolean settingsEnabled) { if (mSettingsButton != null) { mSettingsButton.setEnabled(settingsEnabled); settingsEnabled = settingsEnabled && (mSettingsView == null); mSettingsButton.setVisibility(settingsEnabled ? View.VISIBLE : View.GONE); } } public void refreshLayout(int layoutDirection) { // Force asset reloading mSettingsButton.setImageDrawable(null); mSettingsButton.setImageResource(SystemR.drawable.ic_notify_settings); // Force asset reloading mNotificationButton.setImageDrawable(null); mNotificationButton.setImageResource(SystemR.drawable.ic_notifications); } }