/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.phone; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ResolveInfo; import android.telephony.TelephonyManager; import android.text.Layout; import android.text.TextUtils; import android.util.AttributeSet; import android.util.TypedValue; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewAnimationUtils; import android.view.ViewGroup; import android.view.accessibility.AccessibilityManager; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.widget.Button; import android.widget.FrameLayout; import android.widget.TextView; import java.util.List; public class EmergencyActionGroup extends FrameLayout implements View.OnClickListener { private static final long HIDE_DELAY = 3000; private static final int RIPPLE_DURATION = 600; private static final long RIPPLE_PAUSE = 1000; private final Interpolator mFastOutLinearInInterpolator; private ViewGroup mSelectedContainer; private TextView mSelectedLabel; private View mRippleView; private View mLaunchHint; private View mLastRevealed; private MotionEvent mPendingTouchEvent; private boolean mHiding; public EmergencyActionGroup(Context context, @Nullable AttributeSet attrs) { super(context, attrs); mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_linear_in); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override protected void onFinishInflate() { super.onFinishInflate(); mSelectedContainer = (ViewGroup) findViewById(R.id.selected_container); mSelectedContainer.setOnClickListener(this); mSelectedLabel = (TextView) findViewById(R.id.selected_label); mSelectedLabel.addOnLayoutChangeListener(mLayoutChangeListener); mRippleView = findViewById(R.id.ripple_view); mLaunchHint = findViewById(R.id.launch_hint); mLaunchHint.addOnLayoutChangeListener(mLayoutChangeListener); } @Override protected void onWindowVisibilityChanged(int visibility) { super.onWindowVisibilityChanged(visibility); if (visibility == View.VISIBLE) { setupAssistActions(); } } /** * Called by the activity before a touch event is dispatched to the view hierarchy. */ public void onPreTouchEvent(MotionEvent event) { mPendingTouchEvent = event; } @Override public boolean dispatchTouchEvent(MotionEvent event) { boolean handled = super.dispatchTouchEvent(event); if (mPendingTouchEvent == event && handled) { mPendingTouchEvent = null; } return handled; } /** * Called by the activity after a touch event is dispatched to the view hierarchy. */ public void onPostTouchEvent(MotionEvent event) { // Hide the confirmation button if a touch event was delivered to the activity but not to // this view. if (mPendingTouchEvent != null) { hideTheButton(); } mPendingTouchEvent = null; } private void setupAssistActions() { int[] buttonIds = new int[] {R.id.action1, R.id.action2, R.id.action3}; List infos; if (TelephonyManager.EMERGENCY_ASSISTANCE_ENABLED) { infos = EmergencyAssistanceHelper.resolveAssistPackageAndQueryActivities(getContext()); } else { infos = null; } for (int i = 0; i < 3; i++) { Button button = (Button) findViewById(buttonIds[i]); boolean visible = false; button.setOnClickListener(this); if (infos != null && infos.size() > i && infos.get(i) != null) { ResolveInfo info = infos.get(i); ComponentName name = EmergencyAssistanceHelper.getComponentName(info); button.setTag(R.id.tag_intent, new Intent(EmergencyAssistanceHelper.getIntentAction()) .setComponent(name)); button.setText(info.loadLabel(getContext().getPackageManager())); visible = true; } button.setVisibility(visible ? View.VISIBLE : View.GONE); } } @Override public void onClick(View v) { Intent intent = (Intent) v.getTag(R.id.tag_intent); if (v.getId() == R.id.action1 || v.getId() == R.id.action2 || v.getId() == R.id.action3) { AccessibilityManager accessibilityMgr = (AccessibilityManager) getContext().getSystemService( Context.ACCESSIBILITY_SERVICE); if (accessibilityMgr.isTouchExplorationEnabled()) { getContext().startActivity(intent); } else { revealTheButton(v); } } else if (v.getId() == R.id.selected_container) { if (!mHiding) { getContext().startActivity(intent); } } } private void revealTheButton(View v) { CharSequence buttonText = ((Button) v).getText(); mSelectedLabel.setText(buttonText); mSelectedLabel.setAutoSizeTextTypeWithDefaults(TextView.AUTO_SIZE_TEXT_TYPE_UNIFORM); // In order to trigger OnLayoutChangeListener for reset default minimum font size. mSelectedLabel.requestLayout(); mLaunchHint.requestLayout(); mSelectedContainer.setVisibility(VISIBLE); int centerX = v.getLeft() + v.getWidth() / 2; int centerY = v.getTop() + v.getHeight() / 2; Animator reveal = ViewAnimationUtils.createCircularReveal( mSelectedContainer, centerX, centerY, 0, Math.max(centerX, mSelectedContainer.getWidth() - centerX) + Math.max(centerY, mSelectedContainer.getHeight() - centerY)); reveal.start(); animateHintText(mSelectedLabel, v, reveal); animateHintText(mLaunchHint, v, reveal); mSelectedContainer.setTag(R.id.tag_intent, v.getTag(R.id.tag_intent)); mLastRevealed = v; postDelayed(mHideRunnable, HIDE_DELAY); postDelayed(mRippleRunnable, RIPPLE_PAUSE / 2); // Transfer focus from the originally clicked button to the expanded button. mSelectedContainer.requestFocus(); } private final OnLayoutChangeListener mLayoutChangeListener = new OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { decreaseAutoSizeMinTextSize(v); } }; /** * Prevent some localization string will be truncated if there is low resolution screen * or font size and display size of setting is largest. */ private void decreaseAutoSizeMinTextSize(View selectedView) { if (selectedView != null) { if (selectedView instanceof TextView) { TextView textView = (TextView) selectedView; textView.setEllipsize(TextUtils.TruncateAt.END); // The textView layout will be null due to it's property is hiding when // initialization. Layout layout = textView.getLayout(); if (layout != null) { if (layout.getEllipsisCount(textView.getMaxLines() - 1) > 0) { textView.setAutoSizeTextTypeUniformWithConfiguration( 8, textView.getAutoSizeMaxTextSize(), textView.getAutoSizeStepGranularity(), TypedValue.COMPLEX_UNIT_SP); textView.setGravity(Gravity.CENTER); } } } } } private void animateHintText(View selectedView, View v, Animator reveal) { selectedView.setTranslationX( (v.getLeft() + v.getWidth() / 2 - mSelectedContainer.getWidth() / 2) / 5); selectedView.animate() .setDuration(reveal.getDuration() / 3) .setStartDelay(reveal.getDuration() / 5) .translationX(0) .setInterpolator(mFastOutLinearInInterpolator) .start(); } private void hideTheButton() { if (mHiding || mSelectedContainer.getVisibility() != VISIBLE) { return; } mHiding = true; removeCallbacks(mHideRunnable); View v = mLastRevealed; int centerX = v.getLeft() + v.getWidth() / 2; int centerY = v.getTop() + v.getHeight() / 2; Animator reveal = ViewAnimationUtils.createCircularReveal( mSelectedContainer, centerX, centerY, Math.max(centerX, mSelectedContainer.getWidth() - centerX) + Math.max(centerY, mSelectedContainer.getHeight() - centerY), 0); reveal.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mSelectedContainer.setVisibility(INVISIBLE); removeCallbacks(mRippleRunnable); mHiding = false; } }); reveal.start(); // Transfer focus back to the originally clicked button. if (mSelectedContainer.isFocused()) { v.requestFocus(); } } private void startRipple() { final View ripple = mRippleView; ripple.animate().cancel(); ripple.setVisibility(VISIBLE); Animator reveal = ViewAnimationUtils.createCircularReveal( ripple, ripple.getLeft() + ripple.getWidth() / 2, ripple.getTop() + ripple.getHeight() / 2, 0, ripple.getWidth() / 2); reveal.setDuration(RIPPLE_DURATION); reveal.start(); ripple.setAlpha(0); ripple.animate().alpha(1).setDuration(RIPPLE_DURATION / 2) .withEndAction(new Runnable() { @Override public void run() { ripple.animate().alpha(0).setDuration(RIPPLE_DURATION / 2) .withEndAction(new Runnable() { @Override public void run() { ripple.setVisibility(INVISIBLE); postDelayed(mRippleRunnable, RIPPLE_PAUSE); } }).start(); } }).start(); } private final Runnable mHideRunnable = new Runnable() { @Override public void run() { if (!isAttachedToWindow()) return; hideTheButton(); } }; private final Runnable mRippleRunnable = new Runnable() { @Override public void run() { if (!isAttachedToWindow()) return; startRipple(); } }; }