1 /* 2 * Copyright (C) 2014 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.incallui; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.app.Activity; 22 import android.app.Fragment; 23 import android.app.FragmentManager; 24 import android.graphics.Outline; 25 import android.graphics.Point; 26 import android.os.Bundle; 27 import android.view.Display; 28 import android.view.LayoutInflater; 29 import android.view.View; 30 import android.view.ViewAnimationUtils; 31 import android.view.ViewGroup; 32 import android.view.ViewOutlineProvider; 33 import android.view.ViewTreeObserver; 34 import android.view.ViewTreeObserver.OnPreDrawListener; 35 36 import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette; 37 import com.android.dialer.R; 38 39 public class CircularRevealFragment extends Fragment { 40 static final String TAG = "CircularRevealFragment"; 41 42 private Point mTouchPoint; 43 private OnCircularRevealCompleteListener mListener; 44 private boolean mAnimationStarted; 45 46 interface OnCircularRevealCompleteListener { 47 public void onCircularRevealComplete(FragmentManager fm); 48 } 49 50 public static void startCircularReveal(FragmentManager fm, Point touchPoint, 51 OnCircularRevealCompleteListener listener) { 52 if (fm.findFragmentByTag(TAG) == null) { 53 fm.beginTransaction().add(R.id.main, 54 new CircularRevealFragment(touchPoint, listener), TAG) 55 .commitAllowingStateLoss(); 56 } else { 57 Log.w(TAG, "An instance of CircularRevealFragment already exists"); 58 } 59 } 60 61 public static void endCircularReveal(FragmentManager fm) { 62 final Fragment fragment = fm.findFragmentByTag(TAG); 63 if (fragment != null) { 64 fm.beginTransaction().remove(fragment).commitAllowingStateLoss(); 65 } 66 } 67 68 /** 69 * Empty constructor used only by the {@link FragmentManager}. 70 */ 71 public CircularRevealFragment() {} 72 73 public CircularRevealFragment(Point touchPoint, OnCircularRevealCompleteListener listener) { 74 mTouchPoint = touchPoint; 75 mListener = listener; 76 } 77 78 @Override 79 public void onResume() { 80 super.onResume(); 81 if (!mAnimationStarted) { 82 // Only run the animation once for each instance of the fragment 83 startOutgoingAnimation(InCallPresenter.getInstance().getThemeColors()); 84 } 85 mAnimationStarted = true; 86 } 87 88 @Override 89 public View onCreateView(LayoutInflater inflater, ViewGroup container, 90 Bundle savedInstanceState) { 91 return inflater.inflate(R.layout.outgoing_call_animation, container, false); 92 } 93 94 public void startOutgoingAnimation(MaterialPalette palette) { 95 final Activity activity = getActivity(); 96 if (activity == null) { 97 Log.w(this, "Asked to do outgoing call animation when not attached"); 98 return; 99 } 100 101 final View view = activity.getWindow().getDecorView(); 102 103 // The circle starts from an initial size of 0 so clip it such that it is invisible. 104 // Otherwise the first frame is drawn with a fully opaque screen which causes jank. When 105 // the animation later starts, this clip will be clobbered by the circular reveal clip. 106 // See ViewAnimationUtils.createCircularReveal. 107 view.setOutlineProvider(new ViewOutlineProvider() { 108 @Override 109 public void getOutline(View view, Outline outline) { 110 // Using (0, 0, 0, 0) will not work since the outline will simply be treated as 111 // an empty outline. 112 outline.setOval(-1, -1, 0, 0); 113 } 114 }); 115 view.setClipToOutline(true); 116 117 if (palette != null) { 118 view.findViewById(R.id.outgoing_call_animation_circle).setBackgroundColor( 119 palette.mPrimaryColor); 120 activity.getWindow().setStatusBarColor(palette.mSecondaryColor); 121 } 122 123 view.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() { 124 @Override 125 public boolean onPreDraw() { 126 final ViewTreeObserver vto = view.getViewTreeObserver(); 127 if (vto.isAlive()) { 128 vto.removeOnPreDrawListener(this); 129 } 130 final Animator animator = getRevealAnimator(mTouchPoint); 131 if (animator != null) { 132 animator.addListener(new AnimatorListenerAdapter() { 133 @Override 134 public void onAnimationEnd(Animator animation) { 135 view.setClipToOutline(false); 136 if (mListener != null) { 137 mListener.onCircularRevealComplete(getFragmentManager()); 138 } 139 } 140 }); 141 animator.start(); 142 } 143 return false; 144 } 145 }); 146 } 147 148 private Animator getRevealAnimator(Point touchPoint) { 149 final Activity activity = getActivity(); 150 if (activity == null) { 151 return null; 152 } 153 final View view = activity.getWindow().getDecorView(); 154 final Display display = activity.getWindowManager().getDefaultDisplay(); 155 final Point size = new Point(); 156 display.getSize(size); 157 158 int startX = size.x / 2; 159 int startY = size.y / 2; 160 if (touchPoint != null) { 161 startX = touchPoint.x; 162 startY = touchPoint.y; 163 } 164 165 final Animator valueAnimator = ViewAnimationUtils.createCircularReveal(view, 166 startX, startY, 0, Math.max(size.x, size.y)); 167 valueAnimator.setDuration(getResources().getInteger(R.integer.reveal_animation_duration)); 168 return valueAnimator; 169 } 170 } 171