package tk.wasdennnoch.androidn_ify.systemui.notifications.stack;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.animation.TimeAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.OverScroller;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XC_MethodReplacement;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import tk.wasdennnoch.androidn_ify.R;
import tk.wasdennnoch.androidn_ify.XposedHook;
import tk.wasdennnoch.androidn_ify.extracted.systemui.FakeShadowView;
import tk.wasdennnoch.androidn_ify.extracted.systemui.Interpolators;
import tk.wasdennnoch.androidn_ify.misc.SafeOnPreDrawListener;
import tk.wasdennnoch.androidn_ify.systemui.notifications.NotificationPanelHooks;
import tk.wasdennnoch.androidn_ify.utils.ConfigUtils;
import tk.wasdennnoch.androidn_ify.utils.ResourceUtils;
import static tk.wasdennnoch.androidn_ify.XposedHook.PACKAGE_SYSTEMUI;
public class NotificationStackScrollLayoutHooks implements View.OnApplyWindowInsetsListener {
private static final String TAG = "NotificationStackScrollLayoutHooks";
private static final int ANIMATION_DURATION_STANDARD = 360;
private static final float BACKGROUND_ALPHA_DIMMED = 0.7f;
public static final int ANIMATION_DURATION_DIMMED_ACTIVATED = 220;
private int TAG_ANIMATOR_TRANSLATION_Y;
private int TAG_END_TRANSLATION_Y;
private Class<?> classActivatableNotificationView;
private Class<?> classStackStateAnimator;
private ViewGroup mStackScrollLayout;
private Context mContext;
private ResourceUtils mRes;
private final Paint mBackgroundPaint = new Paint();
private OverScroller mScroller;
private Object mAmbientState;
private Object mSwipeHelper;
private ArrayList<View> mDraggedViews;
private boolean mDisallowDismissInThisMotion;
private boolean mAnimationRunning;
private boolean mAnimationsEnabled = true;
private boolean mDontClampNextScroll;
private boolean mContinuousShadowUpdate;
private boolean mIsExpanded;
private float mDimAmount;
private float mBackgroundFadeAmount = 1.0f;
private int mBgColor;
private ValueAnimator mDimAnimator;
private ValueAnimator.AnimatorUpdateListener mDimUpdateListener
= new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
setDimAmount((Float) animation.getAnimatedValue());
}
};
private Animator.AnimatorListener mDimEndListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mDimAnimator = null;
}
};
private ViewTreeObserver.OnPreDrawListener mBackgroundUpdater = new SafeOnPreDrawListener() {
@Override
public boolean onPreDrawSafe() {
updateBackground();
return true;
}
};
private ViewTreeObserver.OnPreDrawListener mShadowUpdater = new SafeOnPreDrawListener() {
@Override
public boolean onPreDrawSafe() {
updateViewShadows();
return true;
}
};
private Rect mBackgroundBounds = new Rect();
private Rect mStartAnimationRect = new Rect();
private Rect mEndAnimationRect = new Rect();
private Rect mCurrentBounds = new Rect(-1, -1, -1, -1);
private boolean mAnimateNextBackgroundBottom;
private boolean mAnimateNextBackgroundTop;
private ObjectAnimator mBottomAnimator = null;
private ObjectAnimator mTopAnimator = null;
private FrameLayout mFirstVisibleBackgroundChild = null;
private FrameLayout mLastVisibleBackgroundChild = null;
private int mBottomInset = 0;
private int mTopPadding;
private float mStackTranslation;
private View mForcedScroll = null;
private PorterDuffXfermode mSrcMode = new PorterDuffXfermode(PorterDuff.Mode.SRC);
private ArrayList<View> mTmpSortedChildren = new ArrayList<>();
private Comparator<View> mViewPositionComparator = new Comparator<View>() {
@Override
public int compare(View view, View otherView) {
float endY = view.getTranslationY() + XposedHelpers.getIntField(view, "mActualHeight");
float otherEndY = otherView.getTranslationY() + XposedHelpers.getIntField(otherView, "mActualHeight");
if (endY < otherEndY) {
return -1;
} else if (endY > otherEndY) {
return 1;
} else {
// The two notifications end at the same location
return 0;
}
}
};
public NotificationStackScrollLayoutHooks(ClassLoader classLoader) {
try {
Class classNotificationStackScrollLayout = XposedHelpers.findClass("com.android.systemui.statusbar.stack.NotificationStackScrollLayout", classLoader);
XposedBridge.hookAllMethods(classNotificationStackScrollLayout, "initView", new XC_MethodHook() {
@SuppressWarnings("unchecked")
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
mStackScrollLayout = (ViewGroup) param.thisObject;
mScroller = (OverScroller) XposedHelpers.getObjectField(param.thisObject, "mScroller");
mAmbientState = XposedHelpers.getObjectField(param.thisObject, "mAmbientState");
mDraggedViews = (ArrayList<View>) XposedHelpers.getObjectField(mAmbientState, "mDraggedViews");
mSwipeHelper = XposedHelpers.getObjectField(param.thisObject, "mSwipeHelper");
mContext = (Context) param.args[0];
mRes = ResourceUtils.getInstance(mContext);
mBgColor = mRes.getColor(R.color.notification_shade_background_color);
initView();
hookSwipeHelper();
}
});
XposedHelpers.findAndHookMethod(classNotificationStackScrollLayout, "onDraw", Canvas.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
if (ConfigUtils.notifications().enable_notifications_background) {
Canvas canvas = (Canvas) param.args[0];
canvas.drawRect(0, mCurrentBounds.top, mStackScrollLayout.getWidth(), mCurrentBounds.bottom, mBackgroundPaint);
}
}
});
XposedHelpers.findAndHookMethod(classNotificationStackScrollLayout, "startAnimationToState", new XC_MethodHook() {
private boolean willUpdateBackground = false;
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
willUpdateBackground = false;
boolean mNeedsAnimation = XposedHelpers.getBooleanField(mStackScrollLayout, "mNeedsAnimation");
if (mNeedsAnimation) {
XposedHelpers.callMethod(mStackScrollLayout, "generateChildHierarchyEvents");
XposedHelpers.setBooleanField(mStackScrollLayout, "mNeedsAnimation", false);
}
Object mAnimationEvents = XposedHelpers.getObjectField(mStackScrollLayout, "mAnimationEvents");
boolean isEmpty = (boolean) XposedHelpers.callMethod(mAnimationEvents, "isEmpty");
boolean isCurrentlyAnimating = (boolean) XposedHelpers.callMethod(mStackScrollLayout, "isCurrentlyAnimating");
if (!isEmpty || isCurrentlyAnimating) {
setAnimationRunning(true);
willUpdateBackground = true;
}
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
if (willUpdateBackground) {
updateBackground();
updateViewShadows();
willUpdateBackground = false;
}
}
});
XposedHelpers.findAndHookMethod(classNotificationStackScrollLayout, "onChildAnimationFinished", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
setAnimationRunning(false);
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
updateBackground();
}
});
XposedHelpers.findAndHookMethod(classNotificationStackScrollLayout, "applyCurrentState", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
setAnimationRunning(false);
updateBackground();
updateViewShadows();
}
});
XposedHelpers.findAndHookMethod(classNotificationStackScrollLayout, "onLayout", boolean.class, int.class, int.class, int.class, int.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
updateFirstAndLastBackgroundViews();
}
});
XposedHelpers.findAndHookMethod(classNotificationStackScrollLayout, "setAnimationsEnabled", boolean.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
mAnimationsEnabled = (boolean) param.args[0];
}
});
XposedHelpers.findAndHookMethod(classNotificationStackScrollLayout, "setIsExpanded", boolean.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
mIsExpanded = (boolean) param.args[0];
}
});
XposedHelpers.findAndHookMethod(classNotificationStackScrollLayout, "setTopPadding", int.class, boolean.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
mTopPadding = (int) param.args[0];
}
});
XposedHelpers.findAndHookMethod(classNotificationStackScrollLayout, "setStackTranslation", float.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
mStackTranslation = (float) param.args[0];
}
});
XposedHelpers.findAndHookMethod(classNotificationStackScrollLayout, "updateSwipeProgress", View.class, boolean.class, float.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
param.setResult(true); // Don't fade out the notification
}
});
XposedHelpers.findAndHookMethod(classNotificationStackScrollLayout, "onChildSnappedBack", View.class, new XC_MethodHook() {
@SuppressWarnings("SuspiciousMethodCalls")
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
mDraggedViews.remove(param.args[0]);
updateContinuousShadowDrawing();
}
});
XposedHelpers.findAndHookMethod(classNotificationStackScrollLayout, "onBeginDrag", View.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
updateContinuousShadowDrawing();
}
});
XposedHelpers.findAndHookMethod(classNotificationStackScrollLayout, "updateSpeedBumpIndex", int.class, new XC_MethodReplacement() {
@Override
protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
int newIndex = (int) param.args[0];
XposedHelpers.callMethod(mAmbientState, "setSpeedBumpIndex", newIndex);
return null;
}
});
XposedHelpers.findAndHookMethod(classNotificationStackScrollLayout, "initDownStates", MotionEvent.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
MotionEvent ev = (MotionEvent) param.args[0];
if (ev.getAction() == MotionEvent.ACTION_DOWN)
mDisallowDismissInThisMotion = false;
}
});
XposedHelpers.findAndHookMethod(classNotificationStackScrollLayout, "onScrollTouch", MotionEvent.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
mForcedScroll = null;
}
});
XposedHelpers.findAndHookMethod(classNotificationStackScrollLayout, "updateChildren", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
updateForcedScroll();
}
});
XposedHelpers.findAndHookMethod(classNotificationStackScrollLayout, "computeScroll", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
if (!mScroller.isFinished()) {
mDontClampNextScroll = false;
}
}
});
XposedHelpers.findAndHookMethod(classNotificationStackScrollLayout, "overScrollBy", int.class, int.class,
int.class, int.class,
int.class, int.class,
int.class, int.class,
boolean.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
if (mDontClampNextScroll) {
int range = (int) param.args[5];
range = Math.max(range, getOwnScrollY());
param.args[5] = range;
}
}
});
XposedBridge.hookAllMethods(classNotificationStackScrollLayout, "setSpeedBumpView", XC_MethodReplacement.DO_NOTHING);
XposedHelpers.findAndHookMethod(classNotificationStackScrollLayout, "updateSpeedBump", boolean.class, XC_MethodReplacement.DO_NOTHING);
XposedHelpers.findAndHookMethod(classNotificationStackScrollLayout, "setDimmed", boolean.class, boolean.class, new XC_MethodReplacement() {
@Override
protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
boolean dimmed = (boolean) param.args[0];
boolean animate = (boolean) param.args[1];
XposedHelpers.callMethod(mAmbientState, "setDimmed", dimmed);
if (animate && mAnimationsEnabled) {
XposedHelpers.setBooleanField(mStackScrollLayout, "mDimmedNeedsAnimation", true);
XposedHelpers.setBooleanField(mStackScrollLayout, "mNeedsAnimation", true);
animateDimmed(dimmed);
} else {
setDimAmount(dimmed ? 1.0f : 0.0f);
}
XposedHelpers.callMethod(mStackScrollLayout, "requestChildrenUpdate");
return null;
}
});
classActivatableNotificationView = XposedHelpers.findClass("com.android.systemui.statusbar.ActivatableNotificationView", classLoader);
classStackStateAnimator = XposedHelpers.findClass("com.android.systemui.statusbar.stack.StackStateAnimator", classLoader);
} catch (Throwable t) {
XposedHook.logE(TAG, "Error hooking NotificationStackScrollLayout", t);
}
}
private void hookSwipeHelper() {
Class classSwipeHelper = mSwipeHelper.getClass();
XC_MethodHook touchEventHook = new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
if (mDisallowDismissInThisMotion && param.thisObject == mSwipeHelper)
param.setResult(false);
}
};
XposedHelpers.findAndHookMethod(classSwipeHelper, "onTouchEvent", MotionEvent.class, touchEventHook);
XposedHelpers.findAndHookMethod(classSwipeHelper, "onInterceptTouchEvent", MotionEvent.class, touchEventHook);
}
private void updateFirstAndLastBackgroundViews() {
FrameLayout firstChild = getFirstChildWithBackground();
FrameLayout lastChild = getLastChildWithBackground();
if (mAnimationsEnabled && mIsExpanded) {
mAnimateNextBackgroundTop = firstChild != mFirstVisibleBackgroundChild;
mAnimateNextBackgroundBottom = lastChild != mLastVisibleBackgroundChild;
} else {
mAnimateNextBackgroundTop = false;
mAnimateNextBackgroundBottom = false;
}
mFirstVisibleBackgroundChild = firstChild;
mLastVisibleBackgroundChild = lastChild;
}
private void initView() {
TAG_ANIMATOR_TRANSLATION_Y = mContext.getResources().getIdentifier("translation_y_animator_tag", "id", PACKAGE_SYSTEMUI);
TAG_END_TRANSLATION_Y = mContext.getResources().getIdentifier("translation_y_animator_end_value_tag", "id", PACKAGE_SYSTEMUI);
mBackgroundPaint.setColor(0xFFEEEEEE);
mBackgroundPaint.setXfermode(mSrcMode);
mStackScrollLayout.setWillNotDraw(false);
mStackScrollLayout.setOnApplyWindowInsetsListener(NotificationStackScrollLayoutHooks.this);
}
private void updateBackground() {
//TODO completely implement this
/*
if (mAmbientState.isDark()) {
return;
}
*/
updateBackgroundBounds();
/*
if (!mCurrentBounds.equals(mBackgroundBounds)) {
mCurrentBounds.set(mBackgroundBounds);
//mScrimController.setExcludedBackgroundArea(mCurrentBounds);
mBackground.setBounds(0, mCurrentBounds.top, mStackScrollLayout.getWidth(), mCurrentBounds.bottom);
mStackScrollLayout.invalidate();
}
*/
if (!mCurrentBounds.equals(mBackgroundBounds)) {
if (mAnimateNextBackgroundTop || mAnimateNextBackgroundBottom || areBoundsAnimating()) {
startBackgroundAnimation();
} else {
mCurrentBounds.set(mBackgroundBounds);
applyCurrentBackgroundBounds();
}
} else {
if (mBottomAnimator != null) {
mBottomAnimator.cancel();
}
if (mTopAnimator != null) {
mTopAnimator.cancel();
}
}
mAnimateNextBackgroundBottom = false;
mAnimateNextBackgroundTop = false;
}
private void startBackgroundAnimation() {
mCurrentBounds.left = mBackgroundBounds.left;
mCurrentBounds.right = mBackgroundBounds.right;
startBottomAnimation();
startTopAnimation();
}
private void startTopAnimation() {
int previousEndValue = mEndAnimationRect.top;
int newEndValue = mBackgroundBounds.top;
ObjectAnimator previousAnimator = mTopAnimator;
if (previousAnimator != null && previousEndValue == newEndValue) {
return;
}
if (!mAnimateNextBackgroundTop) {
// just a local update was performed
if (previousAnimator != null) {
// we need to increase all animation keyframes of the previous animator by the
// relative change to the end value
int previousStartValue = mStartAnimationRect.top;
PropertyValuesHolder[] values = previousAnimator.getValues();
values[0].setIntValues(previousStartValue, newEndValue);
mStartAnimationRect.top = previousStartValue;
mEndAnimationRect.top = newEndValue;
previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
return;
} else {
// no new animation needed, let's just apply the value
setBackgroundTop(newEndValue);
return;
}
}
if (previousAnimator != null) {
previousAnimator.cancel();
}
ObjectAnimator animator = ObjectAnimator.ofInt(this, "backgroundTop",
mCurrentBounds.top, newEndValue);
Interpolator interpolator = Interpolators.FAST_OUT_SLOW_IN;
animator.setInterpolator(interpolator);
animator.setDuration(ANIMATION_DURATION_STANDARD);
// remove the tag when the animation is finished
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mStartAnimationRect.top = -1;
mEndAnimationRect.top = -1;
mTopAnimator = null;
}
});
animator.start();
mStartAnimationRect.top = mCurrentBounds.top;
mEndAnimationRect.top = newEndValue;
mTopAnimator = animator;
}
private void startBottomAnimation() {
int previousStartValue = mStartAnimationRect.bottom;
int previousEndValue = mEndAnimationRect.bottom;
int newEndValue = mBackgroundBounds.bottom;
ObjectAnimator previousAnimator = mBottomAnimator;
if (previousAnimator != null && previousEndValue == newEndValue) {
return;
}
if (!mAnimateNextBackgroundBottom) {
// just a local update was performed
if (previousAnimator != null) {
// we need to increase all animation keyframes of the previous animator by the
// relative change to the end value
PropertyValuesHolder[] values = previousAnimator.getValues();
values[0].setIntValues(previousStartValue, newEndValue);
mStartAnimationRect.bottom = previousStartValue;
mEndAnimationRect.bottom = newEndValue;
previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
return;
} else {
// no new animation needed, let's just apply the value
setBackgroundBottom(newEndValue);
return;
}
}
if (previousAnimator != null) {
previousAnimator.cancel();
}
ObjectAnimator animator = ObjectAnimator.ofInt(this, "backgroundBottom",
mCurrentBounds.bottom, newEndValue);
Interpolator interpolator = Interpolators.FAST_OUT_SLOW_IN;
animator.setInterpolator(interpolator);
animator.setDuration(ANIMATION_DURATION_STANDARD);
// remove the tag when the animation is finished
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mStartAnimationRect.bottom = -1;
mEndAnimationRect.bottom = -1;
mBottomAnimator = null;
}
});
animator.start();
mStartAnimationRect.bottom = mCurrentBounds.bottom;
mEndAnimationRect.bottom = newEndValue;
mBottomAnimator = animator;
}
private void setBackgroundTop(int top) {
mCurrentBounds.top = top;
applyCurrentBackgroundBounds();
}
public void setBackgroundBottom(int bottom) {
mCurrentBounds.bottom = bottom;
applyCurrentBackgroundBounds();
}
private void applyCurrentBackgroundBounds() {
//TODO implement excluded background area in scrim controller
/*
if (!mFadingOut) {
mScrimController.setExcludedBackgroundArea(mCurrentBounds);
}
*/
mStackScrollLayout.invalidate();
}
private boolean areBoundsAnimating() {
return mBottomAnimator != null || mTopAnimator != null;
}
private void updateBackgroundBounds() {
mBackgroundBounds.left = (int) mStackScrollLayout.getX();
mBackgroundBounds.right = (int) (mStackScrollLayout.getX() + mStackScrollLayout.getWidth());
if (!mIsExpanded) {
mBackgroundBounds.top = 0;
mBackgroundBounds.bottom = 0;
}
FrameLayout firstView = mFirstVisibleBackgroundChild;
int top = 0;
if (firstView != null) {
int finalTranslationY = (int) getFinalTranslationY(firstView);
if (mAnimateNextBackgroundTop
|| mTopAnimator == null && mCurrentBounds.top == finalTranslationY
|| mTopAnimator != null && mEndAnimationRect.top == finalTranslationY) {
// we're ending up at the same location as we are now, lets just skip the animation
top = finalTranslationY;
} else {
top = (int) firstView.getTranslationY();
}
}
FrameLayout lastView = mLastVisibleBackgroundChild;
int bottom = 0;
if (lastView != null) {
int finalTranslationY = (int) getFinalTranslationY(lastView);
int finalHeight = getFinalActualHeight(lastView);
int finalBottom = finalTranslationY + finalHeight;
finalBottom = Math.min(finalBottom, mStackScrollLayout.getHeight());
if (mAnimateNextBackgroundBottom
|| mBottomAnimator == null && mCurrentBounds.bottom == finalBottom
|| mBottomAnimator != null && mEndAnimationRect.bottom == finalBottom) {
// we're ending up at the same location as we are now, lets just skip the animation
bottom = finalBottom;
} else {
bottom = (int) (lastView.getTranslationY() + (int) XposedHelpers.callMethod(lastView, "getActualHeight"));
bottom = Math.min(bottom, mStackScrollLayout.getHeight());
}
} else {
top = (int) (mTopPadding + mStackTranslation);
bottom = top;
}
if (NotificationPanelHooks.getStatusBarState() != NotificationPanelHooks.STATE_KEYGUARD) {
mBackgroundBounds.top = (int) Math.max(mTopPadding + mStackTranslation, top);
} else {
// otherwise the animation from the shade to the keyguard will jump as it's maxed
mBackgroundBounds.top = Math.max(0, top);
}
mBackgroundBounds.bottom = Math.min(mStackScrollLayout.getHeight(), Math.max(bottom, top));
}
private FrameLayout getLastChildWithBackground() {
int childCount = mStackScrollLayout.getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
View child = mStackScrollLayout.getChildAt(i);
if (child.getVisibility() != View.GONE
&& instanceOf(child, classActivatableNotificationView)) {
return (FrameLayout) child;
}
}
return null;
}
private FrameLayout getFirstChildWithBackground() {
int childCount = mStackScrollLayout.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = mStackScrollLayout.getChildAt(i);
if (child.getVisibility() != View.GONE
&& instanceOf(child, classActivatableNotificationView)) {
return (FrameLayout) child;
}
}
return null;
}
private void setAnimationRunning(boolean animationRunning) {
if (animationRunning != mAnimationRunning) {
if (animationRunning) {
mStackScrollLayout.getViewTreeObserver().addOnPreDrawListener(mBackgroundUpdater);
} else {
mStackScrollLayout.getViewTreeObserver().removeOnPreDrawListener(mBackgroundUpdater);
}
mAnimationRunning = animationRunning;
updateContinuousShadowDrawing();
}
}
private void updateBackgroundDimming() {
float alpha = BACKGROUND_ALPHA_DIMMED + (1 - BACKGROUND_ALPHA_DIMMED) * (1.0f - mDimAmount);
alpha *= mBackgroundFadeAmount;
// We need to manually blend in the background color
Object mScrimController = XposedHelpers.getObjectField(mStackScrollLayout, "mScrimController");
int scrimColor = getScrimBehindColor(mScrimController);
// SRC_OVER blending Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc
float alphaInv = 1 - alpha;
int color = Color.argb((int) (alpha * 255 + alphaInv * Color.alpha(scrimColor)),
(int) (mBackgroundFadeAmount * Color.red(mBgColor)
+ alphaInv * Color.red(scrimColor)),
(int) (mBackgroundFadeAmount * Color.green(mBgColor)
+ alphaInv * Color.green(scrimColor)),
(int) (mBackgroundFadeAmount * Color.blue(mBgColor)
+ alphaInv * Color.blue(scrimColor)));
mBackgroundPaint.setColor(color);
XposedHelpers.callMethod(mStackScrollLayout, "invalidate");
}
private void setDimAmount(float dimAmount) {
mDimAmount = dimAmount;
updateBackgroundDimming();
}
private void animateDimmed(boolean dimmed) {
if (mDimAnimator != null) {
mDimAnimator.cancel();
}
float target = dimmed ? 1.0f : 0.0f;
if (target == mDimAmount) {
return;
}
mDimAnimator = TimeAnimator.ofFloat(mDimAmount, target);
mDimAnimator.setDuration(ANIMATION_DURATION_DIMMED_ACTIVATED);
mDimAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
mDimAnimator.addListener(mDimEndListener);
mDimAnimator.addUpdateListener(mDimUpdateListener);
mDimAnimator.start();
}
private static int getScrimBehindColor(Object mScrimController) {
Object mScrimBehind = XposedHelpers.getObjectField(mScrimController, "mScrimBehind");
int color = XposedHelpers.getIntField(mScrimBehind, "mScrimColor");
float mViewAlpha = XposedHelpers.getFloatField(mScrimBehind, "mViewAlpha");
color = Color.argb((int) (Color.alpha(color) * mViewAlpha), Color.red(color),
Color.green(color), Color.blue(color));
return color;
}
private void updateContinuousShadowDrawing() {
boolean continuousShadowUpdate = mAnimationRunning
|| !mDraggedViews.isEmpty();
if (continuousShadowUpdate != mContinuousShadowUpdate) {
if (continuousShadowUpdate) {
mStackScrollLayout.getViewTreeObserver().addOnPreDrawListener(mShadowUpdater);
} else {
mStackScrollLayout.getViewTreeObserver().removeOnPreDrawListener(mShadowUpdater);
}
mContinuousShadowUpdate = continuousShadowUpdate;
}
}
@SuppressWarnings("ConstantConditions")
private void updateViewShadows() {
// we need to work around an issue where the shadow would not cast between siblings when
// their z difference is between 0 and 0.1
// Lefts first sort by Z difference
for (int i = 0; i < mStackScrollLayout.getChildCount(); i++) {
View child = mStackScrollLayout.getChildAt(i);
if (child.getVisibility() != View.GONE) {
mTmpSortedChildren.add(child);
}
}
Collections.sort(mTmpSortedChildren, mViewPositionComparator);
// Now lets update the shadow for the views
View previous = null;
for (int i = 0; i < mTmpSortedChildren.size(); i++) {
View expandableView = mTmpSortedChildren.get(i);
float translationZ = expandableView.getTranslationZ();
float otherZ = previous == null ? translationZ : previous.getTranslationZ();
float diff = otherZ - translationZ;
if (diff <= 0.0f || diff >= FakeShadowView.SHADOW_SIBLING_TRESHOLD) {
// There is no fake shadow to be drawn
setFakeShadowIntensity(expandableView, 0.0f, 0.0f, 0, 0);
} else {
float yLocation = previous.getTranslationY() + XposedHelpers.getIntField(previous, "mActualHeight") -
expandableView.getTranslationY();
setFakeShadowIntensity(expandableView,
diff / FakeShadowView.SHADOW_SIBLING_TRESHOLD,
1, (int) yLocation,
getOutlineTranslation(previous));
}
previous = expandableView;
}
mTmpSortedChildren.clear();
}
private int getOutlineTranslation(View expandableOutlineView) {
try {
if (XposedHelpers.getBooleanField(expandableOutlineView, "mCustomOutline")) {
return (int) expandableOutlineView.getTranslationX();
} else {
Rect mOutlineRect = (Rect) XposedHelpers.getObjectField(expandableOutlineView, "mOutlineRect");
return mOutlineRect.left;
}
} catch (Throwable t) {
return 0;
}
}
private void setFakeShadowIntensity(View activatableNotificationView, float shadowIntensity, float outlineAlpha, int shadowYEnd,
int outlineTranslation) {
FakeShadowView mFakeShadow = (FakeShadowView) activatableNotificationView.findViewById(R.id.fake_shadow);
if (mFakeShadow != null)
mFakeShadow.setFakeShadowTranslationZ(shadowIntensity * (activatableNotificationView.getTranslationZ()
+ FakeShadowView.SHADOW_SIBLING_TRESHOLD), outlineAlpha, shadowYEnd,
outlineTranslation);
}
public void requestDisallowDismiss() {
mDisallowDismissInThisMotion = true;
}
@SuppressWarnings("unchecked")
private static <T> T getChildTag(View child, int tag) {
return (T) child.getTag(tag);
}
private float getFinalTranslationY(View view) {
if (view == null) {
return 0;
}
ValueAnimator yAnimator = getChildTag(view, TAG_ANIMATOR_TRANSLATION_Y);
if (yAnimator == null) {
return view.getTranslationY();
} else {
return getChildTag(view, TAG_END_TRANSLATION_Y);
}
}
private int getFinalActualHeight(View view) {
return (int) XposedHelpers.callStaticMethod(classStackStateAnimator, "getFinalActualHeight", view);
}
private boolean instanceOf(Object obj, Class<?> objClass) {
return objClass.isAssignableFrom(obj.getClass());
}
private void updateForcedScroll() {
if (mForcedScroll != null && (!mForcedScroll.hasFocus()
|| !mForcedScroll.isAttachedToWindow())) {
mForcedScroll = null;
}
if (mForcedScroll != null) {
View expandableView = mForcedScroll;
int positionInLinearLayout = getPositionInLinearLayout(expandableView);
int targetScroll = targetScrollForView(expandableView, positionInLinearLayout);
int outOfViewScroll = positionInLinearLayout + getIntrinsicHeight(expandableView);
targetScroll = Math.max(0, Math.min(targetScroll, getScrollRange()));
// Only apply the scroll if we're scrolling the view upwards, or the view is so far up
// that it is not visible anymore.
int mOwnScrollY = getOwnScrollY();
if (mOwnScrollY < targetScroll || outOfViewScroll < mOwnScrollY) {
setOwnScrollY(mOwnScrollY);
}
}
}
public void lockScrollTo(View v) {
if (mForcedScroll == v) {
return;
}
mForcedScroll = v;
scrollTo(v);
}
private boolean scrollTo(View v) {
int positionInLinearLayout = getPositionInLinearLayout(v);
int targetScroll = targetScrollForView(v, positionInLinearLayout);
int outOfViewScroll = positionInLinearLayout + getIntrinsicHeight(v);
// Only apply the scroll if we're scrolling the view upwards, or the view is so far up
// that it is not visible anymore.
int mOwnScrollY = getOwnScrollY();
if (mOwnScrollY < targetScroll || outOfViewScroll < mOwnScrollY) {
mScroller.startScroll(getScrollX(), mOwnScrollY, 0, targetScroll - mOwnScrollY);
dontReportNextOverScroll();
mStackScrollLayout.postInvalidateOnAnimation();
return true;
}
return false;
}
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
mBottomInset = insets.getSystemWindowInsetBottom();
int range = getScrollRange();
if (getOwnScrollY() > range) {
// HACK: We're repeatedly getting staggered insets here while the IME is
// animating away. To work around that we'll wait until things have settled.
mStackScrollLayout.removeCallbacks(mReclamp);
mStackScrollLayout.postDelayed(mReclamp, 50);
} else if (mForcedScroll != null) {
// The scroll was requested before we got the actual inset - in case we need
// to scroll up some more do so now.
scrollTo(mForcedScroll);
}
return insets;
}
private int targetScrollForView(View v, int positionInLinearLayout) {
return positionInLinearLayout + getIntrinsicHeight(v) +
getImeInset() - mStackScrollLayout.getHeight() + mTopPadding;
}
private int getImeInset() {
return Math.max(0, mBottomInset - (mStackScrollLayout.getRootView().getHeight() - mStackScrollLayout.getHeight()));
}
private int getScrollX() {
return XposedHelpers.getIntField(mStackScrollLayout, "mScrollX");
}
private int getOwnScrollY() {
return XposedHelpers.getIntField(mStackScrollLayout, "mOwnScrollY");
}
private void setOwnScrollY(int ownScrollY) {
XposedHelpers.setIntField(mStackScrollLayout, "mOwnScrollY", ownScrollY);
}
private Runnable mReclamp = new Runnable() {
@Override
public void run() {
int range = getScrollRange();
int mOwnScrollY = getOwnScrollY();
mScroller.startScroll(getScrollX(), mOwnScrollY, 0, range - mOwnScrollY);
dontReportNextOverScroll();
mDontClampNextScroll = true;
mStackScrollLayout.postInvalidateOnAnimation();
}
};
private void dontReportNextOverScroll() {
XposedHelpers.setBooleanField(mStackScrollLayout, "mDontReportNextOverScroll", true);
}
private int getIntrinsicHeight(View v) {
return (int) XposedHelpers.callMethod(v, "getIntrinsicHeight");
}
private int getPositionInLinearLayout(View v) {
return (int) XposedHelpers.callMethod(mStackScrollLayout, "getPositionInLinearLayout", v);
}
private int getScrollRange() {
return (int) XposedHelpers.callMethod(mStackScrollLayout, "getScrollRange");
}
}