/* * Copyright (C) 2011 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.contacts.quickcontact; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.content.Context; import android.content.res.Resources; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.animation.AnimationUtils; import android.widget.FrameLayout; import android.widget.PopupWindow; import com.android.contacts.R; import com.android.contacts.test.NeededForReflection; import com.android.contacts.util.SchedulingUtils; /** * Layout containing single child {@link View} which it attempts to center * around {@link #setChildTargetScreen(Rect)}. *

* Updates drawable state to be {@link android.R.attr#state_first} when child is * above target, and {@link android.R.attr#state_last} when child is below * target. Also updates {@link Drawable#setLevel(int)} on child * {@link View#getBackground()} to reflect horizontal center of target. *

* The reason for this approach is because target {@link Rect} is in screen * coordinates disregarding decor insets; otherwise something like * {@link PopupWindow} might work better. */ public class FloatingChildLayout extends FrameLayout { private static final String TAG = "FloatingChildLayout"; private int mFixedTopPosition; private View mChild; private Rect mTargetScreen = new Rect(); private final int mAnimationDuration; /** The phase of the background dim. This is one of the values of {@link BackgroundPhase} */ private int mBackgroundPhase = BackgroundPhase.BEFORE; private ObjectAnimator mBackgroundAnimator = ObjectAnimator.ofInt(this, "backgroundColorAlpha", 0, DIM_BACKGROUND_ALPHA); private interface BackgroundPhase { public static final int BEFORE = 0; public static final int APPEARING_OR_VISIBLE = 1; public static final int DISAPPEARING_OR_GONE = 3; } /** The phase of the contents window. This is one of the values of {@link ForegroundPhase} */ private int mForegroundPhase = ForegroundPhase.BEFORE; private interface ForegroundPhase { public static final int BEFORE = 0; public static final int APPEARING = 1; public static final int IDLE = 2; public static final int DISAPPEARING = 3; public static final int AFTER = 4; } // Black, 50% alpha as per the system default. private static final int DIM_BACKGROUND_ALPHA = 0x7F; public FloatingChildLayout(Context context, AttributeSet attrs) { super(context, attrs); final Resources resources = getResources(); mFixedTopPosition = resources.getDimensionPixelOffset(R.dimen.quick_contact_top_position); mAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime); super.setBackground(new ColorDrawable(0)); } @Override protected void onFinishInflate() { mChild = findViewById(android.R.id.content); mChild.setDuplicateParentStateEnabled(true); // this will be expanded in showChild() mChild.setScaleX(0.5f); mChild.setScaleY(0.5f); mChild.setAlpha(0.0f); } public View getChild() { return mChild; } /** * FloatingChildLayout manages its own background, don't set it. */ @Override public void setBackground(Drawable background) { Log.wtf(TAG, "don't setBackground(), it is managed internally"); } /** * Set {@link Rect} in screen coordinates that {@link #getChild()} should be * centered around. */ public void setChildTargetScreen(Rect targetScreen) { mTargetScreen = targetScreen; requestLayout(); } /** * Return {@link #mTargetScreen} in local window coordinates, taking any * decor insets into account. */ private Rect getTargetInWindow() { final Rect windowScreen = new Rect(); getWindowVisibleDisplayFrame(windowScreen); final Rect target = new Rect(mTargetScreen); target.offset(-windowScreen.left, -windowScreen.top); return target; } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { final View child = mChild; final Rect target = getTargetInWindow(); final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); if (mFixedTopPosition != -1) { // Horizontally centered, vertically fixed position final int childLeft = (getWidth() - childWidth) / 2; final int childTop = mFixedTopPosition; layoutChild(child, childLeft, childTop); } else { // default is centered horizontally around target... final int childLeft = target.centerX() - (childWidth / 2); // ... and vertically aligned a bit below centered final int childTop = target.centerY() - Math.round(childHeight * 0.35f); // when child is outside bounds, nudge back inside final int clampedChildLeft = clampDimension(childLeft, childWidth, getWidth()); final int clampedChildTop = clampDimension(childTop, childHeight, getHeight()); layoutChild(child, clampedChildLeft, clampedChildTop); } } private static int clampDimension(int value, int size, int max) { // when larger than bounds, just center if (size > max) { return (max - size) / 2; } // clamp to bounds return Math.min(Math.max(value, 0), max - size); } private static void layoutChild(View child, int left, int top) { child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight()); } @NeededForReflection public void setBackgroundColorAlpha(int alpha) { setBackgroundColor(alpha << 24); } public void fadeInBackground() { if (mBackgroundPhase == BackgroundPhase.BEFORE) { mBackgroundPhase = BackgroundPhase.APPEARING_OR_VISIBLE; createChildLayer(); SchedulingUtils.doAfterDraw(this, new Runnable() { @Override public void run() { mBackgroundAnimator.setDuration(mAnimationDuration).start(); } }); } } public void fadeOutBackground() { if (mBackgroundPhase == BackgroundPhase.APPEARING_OR_VISIBLE) { mBackgroundPhase = BackgroundPhase.DISAPPEARING_OR_GONE; if (mBackgroundAnimator.isRunning()) { mBackgroundAnimator.reverse(); } else { ObjectAnimator.ofInt(this, "backgroundColorAlpha", DIM_BACKGROUND_ALPHA, 0). setDuration(mAnimationDuration).start(); } } } public boolean isContentFullyVisible() { return mForegroundPhase == ForegroundPhase.IDLE; } /** Begin animating {@link #getChild()} visible. */ public void showContent(final Runnable onAnimationEndRunnable) { if (mForegroundPhase == ForegroundPhase.BEFORE) { mForegroundPhase = ForegroundPhase.APPEARING; animateScale(false, onAnimationEndRunnable); } } /** * Begin animating {@link #getChild()} invisible. Returns false if animation is not valid in * this state */ public boolean hideContent(final Runnable onAnimationEndRunnable) { if (mForegroundPhase == ForegroundPhase.APPEARING || mForegroundPhase == ForegroundPhase.IDLE) { mForegroundPhase = ForegroundPhase.DISAPPEARING; createChildLayer(); animateScale(true, onAnimationEndRunnable); return true; } else { return false; } } private void createChildLayer() { mChild.invalidate(); mChild.setLayerType(LAYER_TYPE_HARDWARE, null); mChild.buildLayer(); } /** Creates the open/close animation */ private void animateScale( final boolean isExitAnimation, final Runnable onAnimationEndRunnable) { mChild.setPivotX(mTargetScreen.centerX() - mChild.getLeft()); mChild.setPivotY(mTargetScreen.centerY() - mChild.getTop()); final int scaleInterpolator = isExitAnimation ? android.R.interpolator.accelerate_quint : android.R.interpolator.decelerate_quint; final float scaleTarget = isExitAnimation ? 0.5f : 1.0f; mChild.animate() .setDuration(mAnimationDuration) .setInterpolator(AnimationUtils.loadInterpolator(getContext(), scaleInterpolator)) .scaleX(scaleTarget) .scaleY(scaleTarget) .alpha(isExitAnimation ? 0.0f : 1.0f) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mChild.setLayerType(LAYER_TYPE_NONE, null); if (isExitAnimation) { if (mForegroundPhase == ForegroundPhase.DISAPPEARING) { mForegroundPhase = ForegroundPhase.AFTER; if (onAnimationEndRunnable != null) onAnimationEndRunnable.run(); } } else { if (mForegroundPhase == ForegroundPhase.APPEARING) { mForegroundPhase = ForegroundPhase.IDLE; if (onAnimationEndRunnable != null) onAnimationEndRunnable.run(); } } } }); } private View.OnTouchListener mOutsideTouchListener; public void setOnOutsideTouchListener(View.OnTouchListener listener) { mOutsideTouchListener = listener; } @Override public boolean onTouchEvent(MotionEvent event) { // at this point, touch wasn't handled by child view; assume outside if (mOutsideTouchListener != null) { return mOutsideTouchListener.onTouch(this, event); } return false; } }