SearchPanelView.java revision f479792e05485a536c3fa68db9d8a71f34591b78
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 17package com.android.systemui; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.ObjectAnimator; 22import android.animation.PropertyValuesHolder; 23import android.app.ActivityOptions; 24import android.app.SearchManager; 25import android.content.ActivityNotFoundException; 26import android.content.ComponentName; 27import android.content.Context; 28import android.content.Intent; 29import android.content.pm.PackageManager; 30import android.content.res.Resources; 31import android.media.AudioAttributes; 32import android.os.AsyncTask; 33import android.os.Bundle; 34import android.os.UserHandle; 35import android.os.Vibrator; 36import android.provider.Settings; 37import android.util.AttributeSet; 38import android.util.Log; 39import android.view.MotionEvent; 40import android.view.View; 41import android.view.animation.AnimationUtils; 42import android.view.animation.Interpolator; 43import android.widget.FrameLayout; 44import android.widget.ImageView; 45 46import com.android.systemui.statusbar.BaseStatusBar; 47import com.android.systemui.statusbar.CommandQueue; 48import com.android.systemui.statusbar.StatusBarPanel; 49import com.android.systemui.statusbar.phone.PhoneStatusBar; 50 51public class SearchPanelView extends FrameLayout implements StatusBarPanel { 52 53 private static final String TAG = "SearchPanelView"; 54 private static final String ASSIST_ICON_METADATA_NAME = 55 "com.android.systemui.action_assist_icon"; 56 57 private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder() 58 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) 59 .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) 60 .build(); 61 62 private final Context mContext; 63 private BaseStatusBar mBar; 64 65 private View mCard; 66 private ImageView mLogo; 67 private View mScrim; 68 69 private int mPeekHeight; 70 private int mThreshold; 71 private boolean mHorizontal; 72 private final Interpolator mLinearOutSlowInInterpolator; 73 private final Interpolator mFastOutLinearInInterpolator; 74 75 private boolean mAnimatingIn; 76 private boolean mAnimatingOut; 77 private boolean mDragging; 78 private boolean mDraggedFarEnough; 79 private float mStartTouch; 80 private float mStartDrag; 81 82 private ObjectAnimator mEnterAnimator; 83 84 private boolean mStartExitAfterAnimatingIn; 85 86 public SearchPanelView(Context context, AttributeSet attrs) { 87 this(context, attrs, 0); 88 } 89 90 public SearchPanelView(Context context, AttributeSet attrs, int defStyle) { 91 super(context, attrs, defStyle); 92 mContext = context; 93 mPeekHeight = context.getResources().getDimensionPixelSize(R.dimen.search_card_peek_height); 94 mThreshold = context.getResources().getDimensionPixelSize(R.dimen.search_panel_threshold); 95 mLinearOutSlowInInterpolator = 96 AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in); 97 mFastOutLinearInInterpolator = 98 AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_linear_in); 99 } 100 101 private void startAssistActivity() { 102 if (!mBar.isDeviceProvisioned()) return; 103 104 // Close Recent Apps if needed 105 mBar.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_SEARCH_PANEL); 106 107 final Intent intent = ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE)) 108 .getAssistIntent(mContext, true, UserHandle.USER_CURRENT); 109 if (intent == null) return; 110 111 try { 112 final ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext, 113 R.anim.search_launch_enter, R.anim.search_launch_exit); 114 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 115 AsyncTask.execute(new Runnable() { 116 @Override 117 public void run() { 118 mContext.startActivityAsUser(intent, opts.toBundle(), 119 new UserHandle(UserHandle.USER_CURRENT)); 120 } 121 }); 122 } catch (ActivityNotFoundException e) { 123 Log.w(TAG, "Activity not found for " + intent.getAction()); 124 } 125 } 126 127 @Override 128 protected void onFinishInflate() { 129 super.onFinishInflate(); 130 mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 131 mCard = findViewById(R.id.search_panel_card); 132 mLogo = (ImageView) findViewById(R.id.search_logo); 133 mScrim = findViewById(R.id.search_panel_scrim); 134 } 135 136 private void maybeSwapSearchIcon() { 137 Intent intent = ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE)) 138 .getAssistIntent(mContext, false, UserHandle.USER_CURRENT); 139 if (intent != null) { 140 ComponentName component = intent.getComponent(); 141 replaceDrawable(mLogo, component, ASSIST_ICON_METADATA_NAME); 142 } else { 143 mLogo.setImageDrawable(null); 144 } 145 } 146 147 public void replaceDrawable(ImageView v, ComponentName component, String name) { 148 if (component != null) { 149 try { 150 PackageManager packageManager = mContext.getPackageManager(); 151 // Look for the search icon specified in the activity meta-data 152 Bundle metaData = packageManager.getActivityInfo( 153 component, PackageManager.GET_META_DATA).metaData; 154 if (metaData != null) { 155 int iconResId = metaData.getInt(name); 156 if (iconResId != 0) { 157 Resources res = packageManager.getResourcesForActivity(component); 158 v.setImageDrawable(res.getDrawable(iconResId)); 159 return; 160 } 161 } 162 } catch (PackageManager.NameNotFoundException e) { 163 Log.w(TAG, "Failed to swap drawable; " 164 + component.flattenToShortString() + " not found", e); 165 } catch (Resources.NotFoundException nfe) { 166 Log.w(TAG, "Failed to swap drawable from " 167 + component.flattenToShortString(), nfe); 168 } 169 } 170 v.setImageDrawable(null); 171 } 172 173 private boolean pointInside(int x, int y, View v) { 174 final int l = v.getLeft(); 175 final int r = v.getRight(); 176 final int t = v.getTop(); 177 final int b = v.getBottom(); 178 return x >= l && x < r && y >= t && y < b; 179 } 180 181 public boolean isInContentArea(int x, int y) { 182 return pointInside(x, y, mCard); 183 } 184 185 private void vibrate() { 186 Context context = getContext(); 187 if (Settings.System.getIntForUser(context.getContentResolver(), 188 Settings.System.HAPTIC_FEEDBACK_ENABLED, 1, UserHandle.USER_CURRENT) != 0) { 189 Resources res = context.getResources(); 190 Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); 191 vibrator.vibrate(res.getInteger(R.integer.config_search_panel_view_vibration_duration), 192 VIBRATION_ATTRIBUTES); 193 } 194 } 195 196 public void show(final boolean show, boolean animate) { 197 if (show) { 198 maybeSwapSearchIcon(); 199 if (getVisibility() != View.VISIBLE) { 200 setVisibility(View.VISIBLE); 201 vibrate(); 202 mCard.setAlpha(1f); 203 if (animate) { 204 startEnterAnimation(); 205 } else { 206 mScrim.setAlpha(1f); 207 if (mHorizontal) { 208 mCard.setX(getWidth() - mPeekHeight); 209 } else { 210 mCard.setY(getHeight() - mPeekHeight); 211 } 212 } 213 } 214 setFocusable(true); 215 setFocusableInTouchMode(true); 216 requestFocus(); 217 } else { 218 if (animate) { 219 startAbortAnimation(); 220 } else { 221 setVisibility(View.INVISIBLE); 222 } 223 } 224 } 225 226 private void startEnterAnimation() { 227 if (mHorizontal) { 228 mCard.setX(getWidth()); 229 } else { 230 mCard.setY(getHeight()); 231 } 232 mAnimatingIn = true; 233 mCard.animate().cancel(); 234 mEnterAnimator = ObjectAnimator.ofFloat(mCard, mHorizontal ? View.X : View.Y, 235 mHorizontal ? mCard.getX() : mCard.getY(), 236 mHorizontal ? getWidth() - mPeekHeight : getHeight() - mPeekHeight); 237 mEnterAnimator.setDuration(300); 238 mEnterAnimator.setStartDelay(50); 239 mEnterAnimator.setInterpolator(mLinearOutSlowInInterpolator); 240 mEnterAnimator.addListener(new AnimatorListenerAdapter() { 241 @Override 242 public void onAnimationEnd(Animator animation) { 243 mEnterAnimator = null; 244 mAnimatingIn = false; 245 if (mStartExitAfterAnimatingIn) { 246 startExitAnimation(); 247 } 248 } 249 }); 250 mEnterAnimator.start(); 251 mScrim.setAlpha(0f); 252 mScrim.animate() 253 .alpha(1f) 254 .setDuration(300) 255 .setStartDelay(50) 256 .setInterpolator(PhoneStatusBar.ALPHA_IN) 257 .start(); 258 259 } 260 261 private void startAbortAnimation() { 262 mCard.animate().cancel(); 263 mAnimatingOut = true; 264 if (mHorizontal) { 265 mCard.animate().x(getWidth()); 266 } else { 267 mCard.animate().y(getHeight()); 268 } 269 mCard.animate() 270 .setDuration(150) 271 .setInterpolator(mFastOutLinearInInterpolator) 272 .withEndAction(new Runnable() { 273 @Override 274 public void run() { 275 mAnimatingOut = false; 276 setVisibility(View.INVISIBLE); 277 } 278 }); 279 mScrim.animate() 280 .alpha(0f) 281 .setDuration(150) 282 .setStartDelay(0) 283 .setInterpolator(PhoneStatusBar.ALPHA_OUT); 284 } 285 286 public void hide(boolean animate) { 287 if (mBar != null) { 288 // This will indirectly cause show(false, ...) to get called 289 mBar.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE); 290 } else { 291 if (animate) { 292 startAbortAnimation(); 293 } else { 294 setVisibility(View.INVISIBLE); 295 } 296 } 297 } 298 299 @Override 300 public boolean dispatchHoverEvent(MotionEvent event) { 301 // Ignore hover events outside of this panel bounds since such events 302 // generate spurious accessibility events with the panel content when 303 // tapping outside of it, thus confusing the user. 304 final int x = (int) event.getX(); 305 final int y = (int) event.getY(); 306 if (x >= 0 && x < getWidth() && y >= 0 && y < getHeight()) { 307 return super.dispatchHoverEvent(event); 308 } 309 return true; 310 } 311 312 /** 313 * Whether the panel is showing, or, if it's animating, whether it will be 314 * when the animation is done. 315 */ 316 public boolean isShowing() { 317 return getVisibility() == View.VISIBLE && !mAnimatingOut; 318 } 319 320 public void setBar(BaseStatusBar bar) { 321 mBar = bar; 322 } 323 324 public boolean isAssistantAvailable() { 325 return ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE)) 326 .getAssistIntent(mContext, false, UserHandle.USER_CURRENT) != null; 327 } 328 329 private float rubberband(float diff) { 330 return Math.signum(diff) * (float) Math.pow(Math.abs(diff), 0.8f); 331 } 332 333 @Override 334 public boolean onTouchEvent(MotionEvent event) { 335 int action = event.getActionMasked(); 336 switch (action) { 337 case MotionEvent.ACTION_DOWN: 338 mStartTouch = mHorizontal ? event.getX() : event.getY(); 339 mDragging = false; 340 mDraggedFarEnough = false; 341 mStartExitAfterAnimatingIn = false; 342 break; 343 case MotionEvent.ACTION_MOVE: 344 float currentTouch = mHorizontal ? event.getX() : event.getY(); 345 if (getVisibility() == View.VISIBLE && !mDragging && 346 (!mAnimatingIn || Math.abs(mStartTouch - currentTouch) > mThreshold)) { 347 mStartDrag = currentTouch; 348 mDragging = true; 349 } 350 if (!mDraggedFarEnough && Math.abs(mStartTouch - currentTouch) > mThreshold) { 351 mDraggedFarEnough = true; 352 } 353 if (mDragging) { 354 if (!mAnimatingIn && !mAnimatingOut) { 355 if (Math.abs(currentTouch - mStartDrag) > mThreshold) { 356 startExitAnimation(); 357 } else { 358 if (mHorizontal) { 359 mCard.setX(getWidth() - mPeekHeight + rubberband( 360 currentTouch - mStartDrag)); 361 } else { 362 mCard.setY(getHeight() - mPeekHeight + rubberband( 363 currentTouch - mStartDrag)); 364 } 365 } 366 } else if (mAnimatingIn ) { 367 float diff = rubberband(currentTouch - mStartDrag); 368 PropertyValuesHolder[] values = mEnterAnimator.getValues(); 369 values[0].setFloatValues( 370 mHorizontal ? getWidth() + diff : getHeight() + diff, 371 mHorizontal 372 ? getWidth() - mPeekHeight + diff 373 : getHeight() - mPeekHeight + diff); 374 mEnterAnimator.setCurrentPlayTime(mEnterAnimator.getCurrentPlayTime()); 375 } 376 } 377 break; 378 case MotionEvent.ACTION_UP: 379 case MotionEvent.ACTION_CANCEL: 380 if (mDraggedFarEnough) { 381 if (mAnimatingIn) { 382 mStartExitAfterAnimatingIn = true; 383 } else { 384 startExitAnimation(); 385 } 386 } else { 387 startAbortAnimation(); 388 } 389 break; 390 } 391 return true; 392 } 393 394 private void startExitAnimation() { 395 if (mAnimatingOut || getVisibility() != View.VISIBLE) { 396 return; 397 } 398 if (mEnterAnimator != null) { 399 mEnterAnimator.cancel(); 400 } 401 mAnimatingOut = true; 402 startAssistActivity(); 403 vibrate(); 404 mCard.animate() 405 .alpha(0f) 406 .withLayer() 407 .setDuration(250) 408 .setInterpolator(PhoneStatusBar.ALPHA_OUT) 409 .withEndAction(new Runnable() { 410 @Override 411 public void run() { 412 mAnimatingOut = false; 413 setVisibility(View.INVISIBLE); 414 } 415 }); 416 mScrim.animate() 417 .alpha(0f) 418 .setDuration(250) 419 .setStartDelay(0) 420 .setInterpolator(PhoneStatusBar.ALPHA_OUT); 421 } 422 423 public void setHorizontal(boolean horizontal) { 424 mHorizontal = horizontal; 425 } 426} 427