ExpandHelper.java revision 89139d74b27305a29ca082c75d94dcbed5f84625
1/*
2 * Copyright (C) 2012 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
17
18package com.android.systemui;
19
20import android.animation.AnimatorSet;
21import android.animation.ObjectAnimator;
22import android.content.Context;
23import android.graphics.RectF;
24import android.util.Log;
25import android.view.MotionEvent;
26import android.view.ScaleGestureDetector;
27import android.view.View;
28import android.view.ViewGroup;
29import android.view.View.OnClickListener;
30import com.android.internal.widget.SizeAdaptiveLayout;
31
32public class ExpandHelper implements Gefingerpoken, OnClickListener {
33    public interface Callback {
34        View getChildAtPosition(MotionEvent ev);
35        View getChildAtPosition(float x, float y);
36        boolean canChildBeExpanded(View v);
37    }
38
39    private static final String TAG = "ExpandHelper";
40    protected static final boolean DEBUG = false;
41    private static final long EXPAND_DURATION = 250;
42    private static final long GLOW_DURATION = 150;
43
44    // Set to false to disable focus-based gestures (two-finger pull).
45    private static final boolean USE_DRAG = true;
46    // Set to false to disable scale-based gestures (both horizontal and vertical).
47    private static final boolean USE_SPAN = true;
48    // Both gestures types may be active at the same time.
49    // At least one gesture type should be active.
50    // A variant of the screwdriver gesture will emerge from either gesture type.
51
52    // amount of overstretch for maximum brightness expressed in U
53    // 2f: maximum brightness is stretching a 1U to 3U, or a 4U to 6U
54    private static final float STRETCH_INTERVAL = 2f;
55
56    // level of glow for a touch, without overstretch
57    // overstretch fills the range (GLOW_BASE, 1.0]
58    private static final float GLOW_BASE = 0.5f;
59
60    @SuppressWarnings("unused")
61    private Context mContext;
62
63    private boolean mStretching;
64    private View mCurrView;
65    private View mCurrViewTopGlow;
66    private View mCurrViewBottomGlow;
67    private float mOldHeight;
68    private float mNaturalHeight;
69    private float mInitialTouchFocusY;
70    private float mInitialTouchSpan;
71    private Callback mCallback;
72    private ScaleGestureDetector mDetector;
73    private ViewScaler mScaler;
74    private ObjectAnimator mScaleAnimation;
75    private AnimatorSet mGlowAnimationSet;
76    private ObjectAnimator mGlowTopAnimation;
77    private ObjectAnimator mGlowBottomAnimation;
78
79    private int mSmallSize;
80    private int mLargeSize;
81    private float mMaximumStretch;
82
83    private class ViewScaler {
84        View mView;
85        public ViewScaler() {}
86        public void setView(View v) {
87            mView = v;
88        }
89        public void setHeight(float h) {
90            if (DEBUG) Log.v(TAG, "SetHeight: setting to " + h);
91            ViewGroup.LayoutParams lp = mView.getLayoutParams();
92            lp.height = (int)h;
93            mView.setLayoutParams(lp);
94            mView.requestLayout();
95        }
96        public float getHeight() {
97            int height = mView.getLayoutParams().height;
98            if (height < 0) {
99                height = mView.getMeasuredHeight();
100            }
101            return (float) height;
102        }
103        public int getNaturalHeight(int maximum) {
104            ViewGroup.LayoutParams lp = mView.getLayoutParams();
105            if (DEBUG) Log.v(TAG, "Inspecting a child of type: " + mView.getClass().getName());
106            int oldHeight = lp.height;
107            lp.height = ViewGroup.LayoutParams.WRAP_CONTENT;
108            mView.setLayoutParams(lp);
109            mView.measure(
110                    View.MeasureSpec.makeMeasureSpec(mView.getMeasuredWidth(),
111                                                     View.MeasureSpec.EXACTLY),
112                    View.MeasureSpec.makeMeasureSpec(maximum,
113                                                     View.MeasureSpec.AT_MOST));
114            lp.height = oldHeight;
115            mView.setLayoutParams(lp);
116            return mView.getMeasuredHeight();
117        }
118    }
119
120    public ExpandHelper(Context context, Callback callback, int small, int large) {
121        mSmallSize = small;
122        mMaximumStretch = mSmallSize * STRETCH_INTERVAL;
123        mLargeSize = large;
124        mContext = context;
125        mCallback = callback;
126        mScaler = new ViewScaler();
127
128        mScaleAnimation = ObjectAnimator.ofFloat(mScaler, "height", 0f);
129        mScaleAnimation.setDuration(EXPAND_DURATION);
130
131        mGlowTopAnimation = ObjectAnimator.ofFloat(null, "alpha", 0f);
132        mGlowBottomAnimation = ObjectAnimator.ofFloat(null, "alpha", 0f);
133        mGlowAnimationSet = new AnimatorSet();
134        mGlowAnimationSet.play(mGlowTopAnimation).with(mGlowBottomAnimation);
135        mGlowAnimationSet.setDuration(GLOW_DURATION);
136
137        mDetector =
138                new ScaleGestureDetector(context,
139                                         new ScaleGestureDetector.SimpleOnScaleGestureListener() {
140            @Override
141            public boolean onScaleBegin(ScaleGestureDetector detector) {
142                if (DEBUG) Log.v(TAG, "onscalebegin()");
143                View v = mCallback.getChildAtPosition(detector.getFocusX(), detector.getFocusY());
144
145                // your fingers have to be somewhat close to the bounds of the view in question
146                mInitialTouchFocusY = detector.getFocusY();
147                mInitialTouchSpan = Math.abs(detector.getCurrentSpan());
148                if (DEBUG) Log.d(TAG, "got mInitialTouchSpan: (" + mInitialTouchSpan + ")");
149
150                mStretching = initScale(v);
151                return mStretching;
152            }
153
154            @Override
155            public boolean onScale(ScaleGestureDetector detector) {
156                if (DEBUG) Log.v(TAG, "onscale() on " + mCurrView);
157
158                // are we scaling or dragging?
159                float span = Math.abs(detector.getCurrentSpan()) - mInitialTouchSpan;
160                span *= USE_SPAN ? 1f : 0f;
161                float drag = detector.getFocusY() - mInitialTouchFocusY;
162                drag *= USE_DRAG ? 1f : 0f;
163                float pull = Math.abs(drag) + Math.abs(span) + 1f;
164                float hand = drag * Math.abs(drag) / pull + span * Math.abs(span) / pull;
165                if (DEBUG) Log.d(TAG, "current span handle is: " + hand);
166                hand = hand + mOldHeight;
167                float target = hand;
168                if (DEBUG) Log.d(TAG, "target is: " + target);
169                hand = hand < mSmallSize ? mSmallSize : (hand > mLargeSize ? mLargeSize : hand);
170                hand = hand > mNaturalHeight ? mNaturalHeight : hand;
171                if (DEBUG) Log.d(TAG, "scale continues: hand =" + hand);
172                mScaler.setHeight(hand);
173
174                // glow if overscale
175                float stretch = (float) Math.abs((target - hand) / mMaximumStretch);
176                float strength = 1f / (1f + (float) Math.pow(Math.E, -1 * ((8f * stretch) - 5f)));
177                if (DEBUG) Log.d(TAG, "stretch: " + stretch + " strength: " + strength);
178                setGlow(GLOW_BASE + strength * (1f - GLOW_BASE));
179                return true;
180            }
181
182            @Override
183            public void onScaleEnd(ScaleGestureDetector detector) {
184                if (DEBUG) Log.v(TAG, "onscaleend()");
185                // I guess we're alone now
186                if (DEBUG) Log.d(TAG, "scale end");
187                finishScale(false);
188            }
189        });
190    }
191    public void setGlow(float glow) {
192        if (!mGlowAnimationSet.isRunning() || glow == 0f) {
193            if (mGlowAnimationSet.isRunning()) {
194                mGlowAnimationSet.cancel();
195            }
196            if (mCurrViewTopGlow != null && mCurrViewBottomGlow != null) {
197                if (glow == 0f || mCurrViewTopGlow.getAlpha() == 0f) {
198                    // animate glow in and out
199                    mGlowTopAnimation.setTarget(mCurrViewTopGlow);
200                    mGlowBottomAnimation.setTarget(mCurrViewBottomGlow);
201                    mGlowTopAnimation.setFloatValues(glow);
202                    mGlowBottomAnimation.setFloatValues(glow);
203                    mGlowAnimationSet.setupStartValues();
204                    mGlowAnimationSet.start();
205                } else {
206                    // set it explicitly in reponse to touches.
207                    mCurrViewTopGlow.setAlpha(glow);
208                    mCurrViewBottomGlow.setAlpha(glow);
209                }
210            }
211        }
212    }
213
214    public boolean onInterceptTouchEvent(MotionEvent ev) {
215        if (DEBUG) Log.d(TAG, "interceptTouch: act=" + (ev.getAction()) +
216                         " stretching=" + mStretching);
217        mDetector.onTouchEvent(ev);
218        return mStretching;
219    }
220
221    public boolean onTouchEvent(MotionEvent ev) {
222        final int action = ev.getAction();
223        if (DEBUG) Log.d(TAG, "touch: act=" + (action) + " stretching=" + mStretching);
224        if (mStretching) {
225            mDetector.onTouchEvent(ev);
226        }
227        switch (action) {
228            case MotionEvent.ACTION_UP:
229            case MotionEvent.ACTION_CANCEL:
230                mStretching = false;
231                clearView();
232                break;
233        }
234        return true;
235    }
236    private boolean initScale(View v) {
237        if (v != null) {
238            if (DEBUG) Log.d(TAG, "scale begins on view: " + v);
239            mStretching = true;
240            setView(v);
241            setGlow(GLOW_BASE);
242            mScaler.setView(v);
243            mOldHeight = mScaler.getHeight();
244            if (mCallback.canChildBeExpanded(v)) {
245                if (DEBUG) Log.d(TAG, "working on an expandable child");
246                mNaturalHeight = mScaler.getNaturalHeight(mLargeSize);
247            } else {
248                if (DEBUG) Log.d(TAG, "working on a non-expandable child");
249                mNaturalHeight = mOldHeight;
250            }
251            if (DEBUG) Log.d(TAG, "got mOldHeight: " + mOldHeight +
252                        " mNaturalHeight: " + mNaturalHeight);
253            v.getParent().requestDisallowInterceptTouchEvent(true);
254        }
255        return mStretching;
256    }
257
258    private void finishScale(boolean force) {
259        float h = mScaler.getHeight();
260        final boolean wasClosed = (mOldHeight == mSmallSize);
261        if (wasClosed) {
262            h = (force || h > mSmallSize) ? mNaturalHeight : mSmallSize;
263        } else {
264            h = (force || h < mNaturalHeight) ? mSmallSize : mNaturalHeight;
265        }
266        if (DEBUG && mCurrView != null) mCurrView.setBackgroundColor(0);
267        if (mScaleAnimation.isRunning()) {
268            mScaleAnimation.cancel();
269        }
270        mScaleAnimation.setFloatValues(h);
271        mScaleAnimation.setupStartValues();
272        mScaleAnimation.start();
273        mStretching = false;
274        setGlow(0f);
275        if (DEBUG) Log.d(TAG, "scale was finished on view: " + mCurrView);
276        clearView();
277    }
278
279    private void clearView() {
280        mCurrView = null;
281        mCurrViewTopGlow = null;
282        mCurrViewBottomGlow = null;
283    }
284
285    private void setView(View v) {
286        mCurrView = v;
287        if (v instanceof ViewGroup) {
288            ViewGroup g = (ViewGroup) v;
289            mCurrViewTopGlow = g.findViewById(R.id.top_glow);
290            mCurrViewBottomGlow = g.findViewById(R.id.bottom_glow);
291            if (DEBUG) {
292                String debugLog = "Looking for glows: " +
293                        (mCurrViewTopGlow != null ? "found top " : "didn't find top") +
294                        (mCurrViewBottomGlow != null ? "found bottom " : "didn't find bottom");
295                Log.v(TAG,  debugLog);
296            }
297        }
298    }
299
300    @Override
301    public void onClick(View v) {
302        initScale(v);
303        finishScale(true);
304
305    }
306}
307