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