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