blob: 555f94860073bd6baa75865298422ef05474c44c [file] [log] [blame]
/*
* 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)}.
* <p>
* 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.
* <p>
* 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;
}
}