1 /* 2 * Copyright (C) 2015 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 package com.android.phone; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.annotation.Nullable; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.pm.ResolveInfo; 26 import android.telephony.TelephonyManager; 27 import android.text.Layout; 28 import android.text.TextUtils; 29 import android.util.AttributeSet; 30 import android.util.TypedValue; 31 import android.view.Gravity; 32 import android.view.MotionEvent; 33 import android.view.View; 34 import android.view.ViewAnimationUtils; 35 import android.view.ViewGroup; 36 import android.view.accessibility.AccessibilityManager; 37 import android.view.animation.AnimationUtils; 38 import android.view.animation.Interpolator; 39 import android.widget.Button; 40 import android.widget.FrameLayout; 41 import android.widget.TextView; 42 43 import java.util.List; 44 45 public class EmergencyActionGroup extends FrameLayout implements View.OnClickListener { 46 47 private static final long HIDE_DELAY = 3000; 48 private static final int RIPPLE_DURATION = 600; 49 private static final long RIPPLE_PAUSE = 1000; 50 51 private final Interpolator mFastOutLinearInInterpolator; 52 53 private ViewGroup mSelectedContainer; 54 private TextView mSelectedLabel; 55 private View mRippleView; 56 private View mLaunchHint; 57 58 private View mLastRevealed; 59 60 private MotionEvent mPendingTouchEvent; 61 62 private boolean mHiding; 63 EmergencyActionGroup(Context context, @Nullable AttributeSet attrs)64 public EmergencyActionGroup(Context context, @Nullable AttributeSet attrs) { 65 super(context, attrs); 66 mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(context, 67 android.R.interpolator.fast_out_linear_in); 68 } 69 70 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)71 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 72 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 73 } 74 75 @Override onFinishInflate()76 protected void onFinishInflate() { 77 super.onFinishInflate(); 78 79 mSelectedContainer = (ViewGroup) findViewById(R.id.selected_container); 80 mSelectedContainer.setOnClickListener(this); 81 mSelectedLabel = (TextView) findViewById(R.id.selected_label); 82 mSelectedLabel.addOnLayoutChangeListener(mLayoutChangeListener); 83 mRippleView = findViewById(R.id.ripple_view); 84 mLaunchHint = findViewById(R.id.launch_hint); 85 mLaunchHint.addOnLayoutChangeListener(mLayoutChangeListener); 86 } 87 88 @Override onWindowVisibilityChanged(int visibility)89 protected void onWindowVisibilityChanged(int visibility) { 90 super.onWindowVisibilityChanged(visibility); 91 if (visibility == View.VISIBLE) { 92 setupAssistActions(); 93 } 94 } 95 96 /** 97 * Called by the activity before a touch event is dispatched to the view hierarchy. 98 */ onPreTouchEvent(MotionEvent event)99 public void onPreTouchEvent(MotionEvent event) { 100 mPendingTouchEvent = event; 101 } 102 103 @Override dispatchTouchEvent(MotionEvent event)104 public boolean dispatchTouchEvent(MotionEvent event) { 105 boolean handled = super.dispatchTouchEvent(event); 106 if (mPendingTouchEvent == event && handled) { 107 mPendingTouchEvent = null; 108 } 109 return handled; 110 } 111 112 /** 113 * Called by the activity after a touch event is dispatched to the view hierarchy. 114 */ onPostTouchEvent(MotionEvent event)115 public void onPostTouchEvent(MotionEvent event) { 116 // Hide the confirmation button if a touch event was delivered to the activity but not to 117 // this view. 118 if (mPendingTouchEvent != null) { 119 hideTheButton(); 120 } 121 mPendingTouchEvent = null; 122 } 123 setupAssistActions()124 private void setupAssistActions() { 125 int[] buttonIds = new int[] {R.id.action1, R.id.action2, R.id.action3}; 126 127 List<ResolveInfo> infos; 128 129 if (TelephonyManager.EMERGENCY_ASSISTANCE_ENABLED) { 130 infos = EmergencyAssistanceHelper.resolveAssistPackageAndQueryActivities(getContext()); 131 } else { 132 infos = null; 133 } 134 135 for (int i = 0; i < 3; i++) { 136 Button button = (Button) findViewById(buttonIds[i]); 137 boolean visible = false; 138 139 button.setOnClickListener(this); 140 141 if (infos != null && infos.size() > i && infos.get(i) != null) { 142 ResolveInfo info = infos.get(i); 143 ComponentName name = EmergencyAssistanceHelper.getComponentName(info); 144 145 button.setTag(R.id.tag_intent, 146 new Intent(EmergencyAssistanceHelper.getIntentAction()) 147 .setComponent(name)); 148 button.setText(info.loadLabel(getContext().getPackageManager())); 149 visible = true; 150 } 151 152 button.setVisibility(visible ? View.VISIBLE : View.GONE); 153 } 154 } 155 156 @Override onClick(View v)157 public void onClick(View v) { 158 Intent intent = (Intent) v.getTag(R.id.tag_intent); 159 160 if (v.getId() == R.id.action1 || v.getId() == R.id.action2 || v.getId() == R.id.action3) { 161 AccessibilityManager accessibilityMgr = 162 (AccessibilityManager) getContext().getSystemService( 163 Context.ACCESSIBILITY_SERVICE); 164 if (accessibilityMgr.isTouchExplorationEnabled()) { 165 getContext().startActivity(intent); 166 } else { 167 revealTheButton(v); 168 } 169 } else if (v.getId() == R.id.selected_container) { 170 if (!mHiding) { 171 getContext().startActivity(intent); 172 } 173 } 174 } 175 revealTheButton(View v)176 private void revealTheButton(View v) { 177 CharSequence buttonText = ((Button) v).getText(); 178 mSelectedLabel.setText(buttonText); 179 mSelectedLabel.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_UNIFORM); 180 181 // In order to trigger OnLayoutChangeListener for reset default minimum font size. 182 mSelectedLabel.requestLayout(); 183 mLaunchHint.requestLayout(); 184 185 mSelectedContainer.setVisibility(VISIBLE); 186 int centerX = v.getLeft() + v.getWidth() / 2; 187 int centerY = v.getTop() + v.getHeight() / 2; 188 Animator reveal = ViewAnimationUtils.createCircularReveal( 189 mSelectedContainer, 190 centerX, 191 centerY, 192 0, 193 Math.max(centerX, mSelectedContainer.getWidth() - centerX) 194 + Math.max(centerY, mSelectedContainer.getHeight() - centerY)); 195 reveal.start(); 196 197 animateHintText(mSelectedLabel, v, reveal); 198 animateHintText(mLaunchHint, v, reveal); 199 200 mSelectedContainer.setTag(R.id.tag_intent, v.getTag(R.id.tag_intent)); 201 mLastRevealed = v; 202 postDelayed(mHideRunnable, HIDE_DELAY); 203 postDelayed(mRippleRunnable, RIPPLE_PAUSE / 2); 204 205 // Transfer focus from the originally clicked button to the expanded button. 206 mSelectedContainer.requestFocus(); 207 } 208 209 210 private final OnLayoutChangeListener mLayoutChangeListener = new OnLayoutChangeListener() { 211 @Override 212 public void onLayoutChange(View v, int left, int top, int right, int bottom, 213 int oldLeft, 214 int oldTop, int oldRight, int oldBottom) { 215 decreaseAutoSizeMinTextSize(v); 216 } 217 }; 218 219 /** 220 * Prevent some localization string will be truncated if there is low resolution screen 221 * or font size and display size of setting is largest. 222 */ decreaseAutoSizeMinTextSize(View selectedView)223 private void decreaseAutoSizeMinTextSize(View selectedView) { 224 if (selectedView != null) { 225 if (selectedView instanceof TextView) { 226 TextView textView = (TextView) selectedView; 227 textView.setEllipsize(TextUtils.TruncateAt.END); 228 229 // The textView layout will be null due to it's property is hiding when 230 // initialization. 231 Layout layout = textView.getLayout(); 232 if (layout != null) { 233 if (layout.getEllipsisCount(textView.getMaxLines() - 1) > 0) { 234 textView.setAutoSizeTextTypeUniformWithConfiguration( 235 8, 236 textView.getAutoSizeMaxTextSize(), 237 textView.getAutoSizeStepGranularity(), 238 TypedValue.COMPLEX_UNIT_SP); 239 textView.setGravity(Gravity.CENTER); 240 } 241 } 242 } 243 } 244 } 245 animateHintText(View selectedView, View v, Animator reveal)246 private void animateHintText(View selectedView, View v, Animator reveal) { 247 selectedView.setTranslationX( 248 (v.getLeft() + v.getWidth() / 2 - mSelectedContainer.getWidth() / 2) / 5); 249 selectedView.animate() 250 .setDuration(reveal.getDuration() / 3) 251 .setStartDelay(reveal.getDuration() / 5) 252 .translationX(0) 253 .setInterpolator(mFastOutLinearInInterpolator) 254 .start(); 255 } 256 hideTheButton()257 private void hideTheButton() { 258 if (mHiding || mSelectedContainer.getVisibility() != VISIBLE) { 259 return; 260 } 261 262 mHiding = true; 263 264 removeCallbacks(mHideRunnable); 265 266 View v = mLastRevealed; 267 int centerX = v.getLeft() + v.getWidth() / 2; 268 int centerY = v.getTop() + v.getHeight() / 2; 269 Animator reveal = ViewAnimationUtils.createCircularReveal( 270 mSelectedContainer, 271 centerX, 272 centerY, 273 Math.max(centerX, mSelectedContainer.getWidth() - centerX) 274 + Math.max(centerY, mSelectedContainer.getHeight() - centerY), 275 0); 276 reveal.addListener(new AnimatorListenerAdapter() { 277 @Override 278 public void onAnimationEnd(Animator animation) { 279 mSelectedContainer.setVisibility(INVISIBLE); 280 removeCallbacks(mRippleRunnable); 281 mHiding = false; 282 } 283 }); 284 reveal.start(); 285 286 // Transfer focus back to the originally clicked button. 287 if (mSelectedContainer.isFocused()) { 288 v.requestFocus(); 289 } 290 } 291 startRipple()292 private void startRipple() { 293 final View ripple = mRippleView; 294 ripple.animate().cancel(); 295 ripple.setVisibility(VISIBLE); 296 Animator reveal = ViewAnimationUtils.createCircularReveal( 297 ripple, 298 ripple.getLeft() + ripple.getWidth() / 2, 299 ripple.getTop() + ripple.getHeight() / 2, 300 0, 301 ripple.getWidth() / 2); 302 reveal.setDuration(RIPPLE_DURATION); 303 reveal.start(); 304 305 ripple.setAlpha(0); 306 ripple.animate().alpha(1).setDuration(RIPPLE_DURATION / 2) 307 .withEndAction(new Runnable() { 308 @Override 309 public void run() { 310 ripple.animate().alpha(0).setDuration(RIPPLE_DURATION / 2) 311 .withEndAction(new Runnable() { 312 @Override 313 public void run() { 314 ripple.setVisibility(INVISIBLE); 315 postDelayed(mRippleRunnable, RIPPLE_PAUSE); 316 } 317 }).start(); 318 } 319 }).start(); 320 } 321 322 private final Runnable mHideRunnable = new Runnable() { 323 @Override 324 public void run() { 325 if (!isAttachedToWindow()) return; 326 hideTheButton(); 327 } 328 }; 329 330 private final Runnable mRippleRunnable = new Runnable() { 331 @Override 332 public void run() { 333 if (!isAttachedToWindow()) return; 334 startRipple(); 335 } 336 }; 337 } 338