Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7ffa04b1 authored by Daniel Sandler's avatar Daniel Sandler
Browse files

Single finger notification expansion.

If you liked these changes...

  Change Ie4e79aa5
  Change I8a6f8606
  Change I824937e9
  Change I957b6d50

You'll love this one!

Change-Id: I5256366175fa7ebc965b1c5df02f10ba802ed977
parent e9e40b43
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -67,7 +67,7 @@
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fadingEdge="none"
            android:overScrollMode="always"
            android:overScrollMode="ifContentScrolls"
            >
            <com.android.systemui.statusbar.policy.NotificationRowLayout
                android:id="@+id/latestItems"
+3 −0
Original line number Diff line number Diff line
@@ -65,5 +65,8 @@

    <!-- Vibration duration for MultiWaveView used in SearchPanelView -->
    <integer translatable="false" name="config_search_panel_view_vibration_duration">20</integer>

    <!-- The length of the vibration when the notificaiotn pops open. -->
    <integer name="one_finger_pop_duration_ms">10</integer>
</resources>
+3 −0
Original line number Diff line number Diff line
@@ -150,4 +150,7 @@

    <!-- Height of the carrier/wifi name label -->
    <dimen name="carrier_label_height">24dp</dimen>

    <!-- The distance you can pull a notificaiton before it pops open -->
    <dimen name="one_finger_pop_limit">32dp</dimen>
</resources>
+1 −0
Original line number Diff line number Diff line
@@ -18,4 +18,5 @@
<resources>
    <item type="id" name="expandable_tag" />
    <item type="id" name="user_expanded_tag" />
    <item type="id" name="user_lock_tag" />
</resources>
+223 −51
Original line number Diff line number Diff line
@@ -22,24 +22,31 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.os.Vibrator;
import android.util.Slog;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.View.OnClickListener;

import java.util.Stack;

public class ExpandHelper implements Gefingerpoken, OnClickListener {
    public interface Callback {
        View getChildAtRawPosition(float x, float y);
        View getChildAtPosition(float x, float y);
        boolean canChildBeExpanded(View v);
        boolean setUserExpandedChild(View v, boolean userxpanded);
        boolean setUserExpandedChild(View v, boolean userExpanded);
        boolean setUserLockedChild(View v, boolean userLocked);
    }

    private static final String TAG = "ExpandHelper";
    protected static final boolean DEBUG = false;
    protected static final boolean DEBUG_SCALE = false;
    protected static final boolean DEBUG_GLOW = false;
    private static final long EXPAND_DURATION = 250;
    private static final long GLOW_DURATION = 150;

@@ -63,6 +70,9 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener {
    private Context mContext;

    private boolean mStretching;
    private boolean mPullingWithOneFinger;
    private boolean mWatchingForPull;
    private boolean mHasPopped;
    private View mEventSource;
    private View mCurrView;
    private View mCurrViewTopGlow;
@@ -70,7 +80,12 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener {
    private float mOldHeight;
    private float mNaturalHeight;
    private float mInitialTouchFocusY;
    private float mInitialTouchY;
    private float mInitialTouchSpan;
    private int mTouchSlop;
    private int mLastMotionY;
    private float mPopLimit;
    private int mPopDuration;
    private Callback mCallback;
    private ScaleGestureDetector mDetector;
    private ViewScaler mScaler;
@@ -78,6 +93,7 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener {
    private AnimatorSet mGlowAnimationSet;
    private ObjectAnimator mGlowTopAnimation;
    private ObjectAnimator mGlowBottomAnimation;
    private Vibrator mVibrator;

    private int mSmallSize;
    private int mLargeSize;
@@ -85,6 +101,8 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener {

    private int mGravity;

    private View mScrollView;

    private class ViewScaler {
        View mView;

@@ -93,7 +111,7 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener {
            mView = v;
        }
        public void setHeight(float h) {
            if (DEBUG) Slog.v(TAG, "SetHeight: setting to " + h);
            if (DEBUG_SCALE) Slog.v(TAG, "SetHeight: setting to " + h);
            ViewGroup.LayoutParams lp = mView.getLayoutParams();
            lp.height = (int)h;
            mView.setLayoutParams(lp);
@@ -108,7 +126,8 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener {
        }
        public int getNaturalHeight(int maximum) {
            ViewGroup.LayoutParams lp = mView.getLayoutParams();
            if (DEBUG) Slog.v(TAG, "Inspecting a child of type: " + mView.getClass().getName());
            if (DEBUG_SCALE) Slog.v(TAG, "Inspecting a child of type: " +
                    mView.getClass().getName());
            int oldHeight = lp.height;
            lp.height = ViewGroup.LayoutParams.WRAP_CONTENT;
            mView.setLayoutParams(lp);
@@ -142,6 +161,8 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener {
        mGravity = Gravity.TOP;
        mScaleAnimation = ObjectAnimator.ofFloat(mScaler, "height", 0f);
        mScaleAnimation.setDuration(EXPAND_DURATION);
        mPopLimit = mContext.getResources().getDimension(R.dimen.one_finger_pop_limit);
        mPopDuration = mContext.getResources().getInteger(R.integer.one_finger_pop_duration_ms);

        AnimatorListenerAdapter glowVisibilityController = new AnimatorListenerAdapter() {
            @Override
@@ -169,38 +190,30 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener {
        mGlowAnimationSet.play(mGlowTopAnimation).with(mGlowBottomAnimation);
        mGlowAnimationSet.setDuration(GLOW_DURATION);

        final ViewConfiguration configuration = ViewConfiguration.get(mContext);
        mTouchSlop = configuration.getScaledTouchSlop();

        mDetector =
                new ScaleGestureDetector(context,
                                         new ScaleGestureDetector.SimpleOnScaleGestureListener() {
            @Override
            public boolean onScaleBegin(ScaleGestureDetector detector) {
                if (DEBUG) Slog.v(TAG, "onscalebegin()");
                if (DEBUG_SCALE) Slog.v(TAG, "onscalebegin()");
                float x = detector.getFocusX();
                float y = detector.getFocusY();

                View v = null;
                if (mEventSource != null) {
                    int[] location = new int[2];
                    mEventSource.getLocationOnScreen(location);
                    x += (float) location[0];
                    y += (float) location[1];
                    v = mCallback.getChildAtRawPosition(x, y);
                } else {
                    v = mCallback.getChildAtPosition(x, y);
                }

                // your fingers have to be somewhat close to the bounds of the view in question
                mInitialTouchFocusY = detector.getFocusY();
                mInitialTouchSpan = Math.abs(detector.getCurrentSpan());
                if (DEBUG) Slog.d(TAG, "got mInitialTouchSpan: (" + mInitialTouchSpan + ")");
                if (DEBUG_SCALE) Slog.d(TAG, "got mInitialTouchSpan: (" + mInitialTouchSpan + ")");

                mStretching = initScale(v);
                mStretching = initScale(findView(x, y));
                return mStretching;
            }

            @Override
            public boolean onScale(ScaleGestureDetector detector) {
                if (DEBUG) Slog.v(TAG, "onscale() on " + mCurrView);
                if (DEBUG_SCALE) Slog.v(TAG, "onscale() on " + mCurrView);

                // are we scaling or dragging?
                float span = Math.abs(detector.getCurrentSpan()) - mInitialTouchSpan;
@@ -210,33 +223,71 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener {
                drag *= mGravity == Gravity.BOTTOM ? -1f : 1f;
                float pull = Math.abs(drag) + Math.abs(span) + 1f;
                float hand = drag * Math.abs(drag) / pull + span * Math.abs(span) / pull;
                if (DEBUG) Slog.d(TAG, "current span handle is: " + hand);
                hand = hand + mOldHeight;
                float target = hand;
                if (DEBUG) Slog.d(TAG, "target is: " + target);
                hand = hand < mSmallSize ? mSmallSize : (hand > mLargeSize ? mLargeSize : hand);
                hand = hand > mNaturalHeight ? mNaturalHeight : hand;
                if (DEBUG) Slog.d(TAG, "scale continues: hand =" + hand);
                mScaler.setHeight(hand);
                float target = hand + mOldHeight;
                float newHeight = clamp(target);
                mScaler.setHeight(newHeight);

                // glow if overscale
                float stretch = (float) Math.abs((target - hand) / mMaximumStretch);
                float strength = 1f / (1f + (float) Math.pow(Math.E, -1 * ((8f * stretch) - 5f)));
                if (DEBUG) Slog.d(TAG, "stretch: " + stretch + " strength: " + strength);
                setGlow(GLOW_BASE + strength * (1f - GLOW_BASE));
                setGlow(calculateGlow(target, newHeight));
                return true;
            }

            @Override
            public void onScaleEnd(ScaleGestureDetector detector) {
                if (DEBUG) Slog.v(TAG, "onscaleend()");
                if (DEBUG_SCALE) Slog.v(TAG, "onscaleend()");
                // I guess we're alone now
                if (DEBUG) Slog.d(TAG, "scale end");
                if (DEBUG_SCALE) Slog.d(TAG, "scale end");
                finishScale(false);
                clearView();
                mStretching = false;
            }
        });
    }

    private float clamp(float target) {
        float out = target;
        out = out < mSmallSize ? mSmallSize : (out > mLargeSize ? mLargeSize : out);
        out = out > mNaturalHeight ? mNaturalHeight : out;
        return out;
    }

    private View findView(float x, float y) {
        View v = null;
        if (mEventSource != null) {
            int[] location = new int[2];
            mEventSource.getLocationOnScreen(location);
            x += (float) location[0];
            y += (float) location[1];
            v = mCallback.getChildAtRawPosition(x, y);
        } else {
            v = mCallback.getChildAtPosition(x, y);
        }
        return v;
    }

    private boolean isInside(View v, float x, float y) {
        if (DEBUG) Slog.d(TAG, "isinside (" + x + ", " + y + ")");

        if (v == null) {
            if (DEBUG) Slog.d(TAG, "isinside null subject");
            return false;
        }
        if (mEventSource != null) {
            int[] location = new int[2];
            mEventSource.getLocationOnScreen(location);
            x += (float) location[0];
            y += (float) location[1];
            if (DEBUG) Slog.d(TAG, "  to global (" + x + ", " + y + ")");
        }
        int[] location = new int[2];
        v.getLocationOnScreen(location);
        x -= (float) location[0];
        y -= (float) location[1];
        if (DEBUG) Slog.d(TAG, "  to local (" + x + ", " + y + ")");
        if (DEBUG) Slog.d(TAG, "  inside (" + v.getWidth() + ", " + v.getHeight() + ")");
        boolean inside = (x > 0f && y > 0f && x < v.getWidth() & y < v.getHeight());
        return inside;
    }

    public void setEventSource(View eventSource) {
        mEventSource = eventSource;
    }
@@ -245,10 +296,23 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener {
        mGravity = gravity;
    }

    public void setScrollView(View scrollView) {
        mScrollView = scrollView;
    }

    private float calculateGlow(float target, float actual) {
        // glow if overscale
        if (DEBUG_GLOW) Slog.d(TAG, "target: " + target + " actual: " + actual);
        float stretch = (float) Math.abs((target - actual) / mMaximumStretch);
        float strength = 1f / (1f + (float) Math.pow(Math.E, -1 * ((8f * stretch) - 5f)));
        if (DEBUG_GLOW) Slog.d(TAG, "stretch: " + stretch + " strength: " + strength);
        return (GLOW_BASE + strength * (1f - GLOW_BASE));
    }

    public void setGlow(float glow) {
        if (!mGlowAnimationSet.isRunning() || glow == 0f) {
            if (mGlowAnimationSet.isRunning()) {
                mGlowAnimationSet.cancel();
                mGlowAnimationSet.end();
            }
            if (mCurrViewTopGlow != null && mCurrViewBottomGlow != null) {
                if (glow == 0f || mCurrViewTopGlow.getAlpha() == 0f) {
@@ -278,23 +342,115 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener {

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (DEBUG) Slog.d(TAG, "interceptTouch: act=" + (ev.getAction()) +
                         " stretching=" + mStretching);
                         " stretching=" + mStretching +
                         " onefinger=" + mPullingWithOneFinger);
        // check for a two-finger gesture
        mDetector.onTouchEvent(ev);
        return mStretching;
        if (mStretching) {
            return true;
        } else {
            final int action = ev.getAction();
            if ((action == MotionEvent.ACTION_MOVE) && mPullingWithOneFinger) {
                return true;
            }
            if (mScrollView != null && mScrollView.getScrollY() > 0) {
                return false;
            }
            switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_MOVE: {
                if (mWatchingForPull) {
                    final int x = (int) ev.getX();
                    final int y = (int) ev.getY();
                    final int yDiff = y - mLastMotionY;
                    if (yDiff > mTouchSlop) {
                        mLastMotionY = y;
                        mPullingWithOneFinger = initScale(findView(x, y));
                        if (mPullingWithOneFinger) {
                            mInitialTouchY = mLastMotionY;
                            mHasPopped = false;
                        }
                    }
                }
                break;
            }

            case MotionEvent.ACTION_DOWN:
                mWatchingForPull = isInside(mScrollView, ev.getX(), ev.getY());
                mLastMotionY = (int) ev.getY();
                break;

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                if (mPullingWithOneFinger) {
                    finishScale(false);
                    clearView();
                }
                mPullingWithOneFinger = false;
                mWatchingForPull = false;
                break;
            }
            return mPullingWithOneFinger;
        }
    }

    public boolean onTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        if (DEBUG) Slog.d(TAG, "touch: act=" + (action) + " stretching=" + mStretching);
        if (DEBUG_SCALE) Slog.d(TAG, "touch: act=" + (action) +
                         " stretching=" + mStretching +
                         " onefinger=" + mPullingWithOneFinger);
        if (mStretching) {
            if (DEBUG) Slog.d(TAG, "detector ontouch");
            mDetector.onTouchEvent(ev);
        }
        switch (action) {
            case MotionEvent.ACTION_MOVE: {
                if (mPullingWithOneFinger) {
                    final float rawHeight = ev.getY() - mInitialTouchY + mOldHeight;
                    final float newHeight = clamp(rawHeight);
                    final boolean wasClosed = (mOldHeight == mSmallSize);
                    boolean isFinished = false;
                    if (rawHeight > mNaturalHeight) {
                        isFinished = true;
                    }
                    if (rawHeight < mSmallSize) {
                        isFinished = true;
                    }

                    final float pull = Math.abs(ev.getY() - mInitialTouchY);
                    if (mHasPopped || pull > mPopLimit) {
                        if (!mHasPopped) {
                            vibrate(mPopDuration);
                            mHasPopped = true;
                        }
                    }

                    if (mHasPopped) {
                        mScaler.setHeight(newHeight);
                        setGlow(GLOW_BASE);
                    } else {
                        setGlow(calculateGlow(4f * pull, 0f));
                    }

                    final int x = (int) ev.getX();
                    final int y = (int) ev.getY();
                    View underPointer = findView(x, y);
                    if (isFinished && underPointer != null && underPointer != mCurrView) {
                        finishScale(false);
                        initScale(underPointer);
                        mInitialTouchY = ev.getY();
                        mHasPopped = false;
                    }
                    return true;
                }
                break;
            }
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (DEBUG) Slog.d(TAG, "cancel");
                mStretching = false;
                if (mPullingWithOneFinger) {
                    finishScale(false);
                    mPullingWithOneFinger = false;
                }
                clearView();
                break;
        }
@@ -303,7 +459,7 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener {
    private boolean initScale(View v) {
        if (v != null) {
            if (DEBUG) Slog.d(TAG, "scale begins on view: " + v);
            mStretching = true;
            mCallback.setUserLockedChild(v, true);
            setView(v);
            setGlow(GLOW_BASE);
            mScaler.setView(v);
@@ -318,30 +474,34 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener {
            if (DEBUG) Slog.d(TAG, "got mOldHeight: " + mOldHeight +
                        " mNaturalHeight: " + mNaturalHeight);
            v.getParent().requestDisallowInterceptTouchEvent(true);
            return true;
        } else {
            return false;
        }
        return mStretching;
    }

    private void finishScale(boolean force) {
        float currentHeight = mScaler.getHeight();
        float targetHeight = mSmallSize;
        float h = mScaler.getHeight();
        final boolean wasClosed = (mOldHeight == mSmallSize);
        if (wasClosed) {
            h = (force || h > mSmallSize) ? mNaturalHeight : mSmallSize;
            targetHeight = (force || currentHeight > mSmallSize) ? mNaturalHeight : mSmallSize;
        } else {
            h = (force || h < mNaturalHeight) ? mSmallSize : mNaturalHeight;
            targetHeight = (force || currentHeight < mNaturalHeight) ? mSmallSize : mNaturalHeight;
        }
        if (DEBUG && mCurrView != null) mCurrView.setBackgroundColor(0);
        if (mScaleAnimation.isRunning()) {
            mScaleAnimation.cancel();
        }
        mScaleAnimation.setFloatValues(h);
        mScaleAnimation.setupStartValues();
        mScaleAnimation.start();
        mStretching = false;
        setGlow(0f);
        mCallback.setUserExpandedChild(mCurrView, h == mNaturalHeight);
        if (targetHeight != currentHeight) {
            mScaleAnimation.setFloatValues(targetHeight);
            mScaleAnimation.setupStartValues();
            mScaleAnimation.start();
        }
        mCallback.setUserLockedChild(mCurrView, false);
        if (DEBUG) Slog.d(TAG, "scale was finished on view: " + mCurrView);
        clearView();
    }

    private void clearView() {
@@ -369,6 +529,18 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener {
    public void onClick(View v) {
        initScale(v);
        finishScale(true);
        clearView();
    }

    /**
     * Triggers haptic feedback.
     */
    private synchronized void vibrate(long duration) {
        if (mVibrator == null) {
            mVibrator = (android.os.Vibrator)
                    mContext.getSystemService(Context.VIBRATOR_SERVICE);
        }
        mVibrator.vibrate(duration);
    }
}
Loading