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