| /* |
| * Copyright (C) 2017 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.systemui.statusbar.notification; |
| |
| import android.content.res.Resources; |
| import android.util.Pools; |
| import android.view.View; |
| import android.view.ViewGroup; |
| |
| import com.android.internal.widget.MessagingGroup; |
| import com.android.internal.widget.MessagingImageMessage; |
| import com.android.internal.widget.MessagingLayout; |
| import com.android.internal.widget.MessagingLinearLayout; |
| import com.android.internal.widget.MessagingMessage; |
| import com.android.internal.widget.MessagingPropertyAnimator; |
| import com.android.systemui.Interpolators; |
| |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| |
| /** |
| * A transform state of the action list |
| */ |
| public class MessagingLayoutTransformState extends TransformState { |
| |
| private static Pools.SimplePool<MessagingLayoutTransformState> sInstancePool |
| = new Pools.SimplePool<>(40); |
| private MessagingLinearLayout mMessageContainer; |
| private MessagingLayout mMessagingLayout; |
| private HashMap<MessagingGroup, MessagingGroup> mGroupMap = new HashMap<>(); |
| private float mRelativeTranslationOffset; |
| |
| public static MessagingLayoutTransformState obtain() { |
| MessagingLayoutTransformState instance = sInstancePool.acquire(); |
| if (instance != null) { |
| return instance; |
| } |
| return new MessagingLayoutTransformState(); |
| } |
| |
| @Override |
| public void initFrom(View view, TransformInfo transformInfo) { |
| super.initFrom(view, transformInfo); |
| if (mTransformedView instanceof MessagingLinearLayout) { |
| mMessageContainer = (MessagingLinearLayout) mTransformedView; |
| mMessagingLayout = mMessageContainer.getMessagingLayout(); |
| Resources resources = view.getContext().getResources(); |
| mRelativeTranslationOffset = resources.getDisplayMetrics().density * 8; |
| } |
| } |
| |
| @Override |
| public boolean transformViewTo(TransformState otherState, float transformationAmount) { |
| if (otherState instanceof MessagingLayoutTransformState) { |
| // It's a party! Let's transform between these two layouts! |
| transformViewInternal((MessagingLayoutTransformState) otherState, transformationAmount, |
| true /* to */); |
| return true; |
| } else { |
| return super.transformViewTo(otherState, transformationAmount); |
| } |
| } |
| |
| @Override |
| public void transformViewFrom(TransformState otherState, float transformationAmount) { |
| if (otherState instanceof MessagingLayoutTransformState) { |
| // It's a party! Let's transform between these two layouts! |
| transformViewInternal((MessagingLayoutTransformState) otherState, transformationAmount, |
| false /* to */); |
| } else { |
| super.transformViewFrom(otherState, transformationAmount); |
| } |
| } |
| |
| private void transformViewInternal(MessagingLayoutTransformState mlt, |
| float transformationAmount, boolean to) { |
| ensureVisible(); |
| ArrayList<MessagingGroup> ownGroups = filterHiddenGroups( |
| mMessagingLayout.getMessagingGroups()); |
| ArrayList<MessagingGroup> otherGroups = filterHiddenGroups( |
| mlt.mMessagingLayout.getMessagingGroups()); |
| HashMap<MessagingGroup, MessagingGroup> pairs = findPairs(ownGroups, otherGroups); |
| MessagingGroup lastPairedGroup = null; |
| float currentTranslation = 0; |
| for (int i = ownGroups.size() - 1; i >= 0; i--) { |
| MessagingGroup ownGroup = ownGroups.get(i); |
| MessagingGroup matchingGroup = pairs.get(ownGroup); |
| if (!isGone(ownGroup)) { |
| if (matchingGroup != null) { |
| transformGroups(ownGroup, matchingGroup, transformationAmount, to); |
| if (lastPairedGroup == null) { |
| lastPairedGroup = ownGroup; |
| if (to){ |
| float totalTranslation = ownGroup.getTop() - matchingGroup.getTop(); |
| currentTranslation = matchingGroup.getAvatar().getTranslationY() |
| - totalTranslation; |
| } else { |
| currentTranslation = ownGroup.getAvatar().getTranslationY(); |
| } |
| } |
| } else { |
| float groupTransformationAmount = transformationAmount; |
| if (lastPairedGroup != null) { |
| adaptGroupAppear(ownGroup, transformationAmount, currentTranslation, |
| to); |
| float newPosition = ownGroup.getTop() + currentTranslation; |
| |
| if (!mTransformInfo.isAnimating()) { |
| // We fade the group away as soon as 1/2 of it is translated away on top |
| float fadeStart = -ownGroup.getHeight() * 0.5f; |
| groupTransformationAmount = (newPosition - fadeStart) |
| / Math.abs(fadeStart); |
| } else { |
| float fadeStart = -ownGroup.getHeight() * 0.75f; |
| // We want to fade out as soon as the animation starts, let's add the |
| // complete top in addition |
| groupTransformationAmount = (newPosition - fadeStart) |
| / (Math.abs(fadeStart) + ownGroup.getTop()); |
| } |
| groupTransformationAmount = Math.max(0.0f, Math.min(1.0f, |
| groupTransformationAmount)); |
| if (to) { |
| groupTransformationAmount = 1.0f - groupTransformationAmount; |
| } |
| } |
| if (to) { |
| disappear(ownGroup, groupTransformationAmount); |
| } else { |
| appear(ownGroup, groupTransformationAmount); |
| } |
| } |
| } |
| } |
| } |
| |
| private void appear(MessagingGroup ownGroup, float transformationAmount) { |
| MessagingLinearLayout ownMessages = ownGroup.getMessageContainer(); |
| for (int j = 0; j < ownMessages.getChildCount(); j++) { |
| View child = ownMessages.getChildAt(j); |
| if (isGone(child)) { |
| continue; |
| } |
| appear(child, transformationAmount); |
| setClippingDeactivated(child, true); |
| } |
| appear(ownGroup.getAvatar(), transformationAmount); |
| appear(ownGroup.getSenderView(), transformationAmount); |
| appear(ownGroup.getIsolatedMessage(), transformationAmount); |
| setClippingDeactivated(ownGroup.getSenderView(), true); |
| setClippingDeactivated(ownGroup.getAvatar(), true); |
| } |
| |
| private void adaptGroupAppear(MessagingGroup ownGroup, float transformationAmount, |
| float overallTranslation, boolean to) { |
| float relativeOffset; |
| if (to) { |
| relativeOffset = transformationAmount * mRelativeTranslationOffset; |
| } else { |
| relativeOffset = (1.0f - transformationAmount) * mRelativeTranslationOffset; |
| } |
| if (ownGroup.getSenderView().getVisibility() != View.GONE) { |
| relativeOffset *= 0.5f; |
| } |
| ownGroup.getMessageContainer().setTranslationY(relativeOffset); |
| ownGroup.getSenderView().setTranslationY(relativeOffset); |
| ownGroup.setTranslationY(overallTranslation * 0.9f); |
| } |
| |
| private void disappear(MessagingGroup ownGroup, float transformationAmount) { |
| MessagingLinearLayout ownMessages = ownGroup.getMessageContainer(); |
| for (int j = 0; j < ownMessages.getChildCount(); j++) { |
| View child = ownMessages.getChildAt(j); |
| if (isGone(child)) { |
| continue; |
| } |
| disappear(child, transformationAmount); |
| setClippingDeactivated(child, true); |
| } |
| disappear(ownGroup.getAvatar(), transformationAmount); |
| disappear(ownGroup.getSenderView(), transformationAmount); |
| disappear(ownGroup.getIsolatedMessage(), transformationAmount); |
| setClippingDeactivated(ownGroup.getSenderView(), true); |
| setClippingDeactivated(ownGroup.getAvatar(), true); |
| } |
| |
| private void appear(View child, float transformationAmount) { |
| if (child == null || child.getVisibility() == View.GONE) { |
| return; |
| } |
| TransformState ownState = TransformState.createFrom(child, mTransformInfo); |
| ownState.appear(transformationAmount, null); |
| ownState.recycle(); |
| } |
| |
| private void disappear(View child, float transformationAmount) { |
| if (child == null || child.getVisibility() == View.GONE) { |
| return; |
| } |
| TransformState ownState = TransformState.createFrom(child, mTransformInfo); |
| ownState.disappear(transformationAmount, null); |
| ownState.recycle(); |
| } |
| |
| private ArrayList<MessagingGroup> filterHiddenGroups( |
| ArrayList<MessagingGroup> groups) { |
| ArrayList<MessagingGroup> result = new ArrayList<>(groups); |
| for (int i = 0; i < result.size(); i++) { |
| MessagingGroup messagingGroup = result.get(i); |
| if (isGone(messagingGroup)) { |
| result.remove(i); |
| i--; |
| } |
| } |
| return result; |
| } |
| |
| private void transformGroups(MessagingGroup ownGroup, MessagingGroup otherGroup, |
| float transformationAmount, boolean to) { |
| boolean useLinearTransformation = |
| otherGroup.getIsolatedMessage() == null && !mTransformInfo.isAnimating(); |
| transformView(transformationAmount, to, ownGroup.getSenderView(), otherGroup.getSenderView(), |
| true /* sameAsAny */, useLinearTransformation); |
| transformView(transformationAmount, to, ownGroup.getAvatar(), otherGroup.getAvatar(), |
| true /* sameAsAny */, useLinearTransformation); |
| List<MessagingMessage> ownMessages = ownGroup.getMessages(); |
| List<MessagingMessage> otherMessages = otherGroup.getMessages(); |
| float previousTranslation = 0; |
| for (int i = 0; i < ownMessages.size(); i++) { |
| View child = ownMessages.get(ownMessages.size() - 1 - i).getView(); |
| if (isGone(child)) { |
| continue; |
| } |
| int otherIndex = otherMessages.size() - 1 - i; |
| View otherChild = null; |
| if (otherIndex >= 0) { |
| otherChild = otherMessages.get(otherIndex).getView(); |
| if (isGone(otherChild)) { |
| otherChild = null; |
| } |
| } |
| if (otherChild == null && previousTranslation < 0) { |
| // Let's fade out as we approach the top of the screen. We can only do this if |
| // we're actually moving up |
| float distanceToTop = child.getTop() + child.getHeight() + previousTranslation; |
| transformationAmount = distanceToTop / child.getHeight(); |
| transformationAmount = Math.max(0.0f, Math.min(1.0f, transformationAmount)); |
| if (to) { |
| transformationAmount = 1.0f - transformationAmount; |
| } |
| } |
| transformView(transformationAmount, to, child, otherChild, false, /* sameAsAny */ |
| useLinearTransformation); |
| boolean otherIsIsolated = otherGroup.getIsolatedMessage() == otherChild; |
| if (transformationAmount == 0.0f && otherIsIsolated) { |
| ownGroup.setTransformingImages(true); |
| } |
| if (otherChild == null) { |
| child.setTranslationY(previousTranslation); |
| setClippingDeactivated(child, true); |
| } else if (ownGroup.getIsolatedMessage() == child || otherIsIsolated) { |
| // We don't want to add any translation for the image that is transforming |
| } else if (to) { |
| float totalTranslation = child.getTop() + ownGroup.getTop() |
| - otherChild.getTop() - otherChild.getTop(); |
| previousTranslation = otherChild.getTranslationY() - totalTranslation; |
| } else { |
| previousTranslation = child.getTranslationY(); |
| } |
| } |
| ownGroup.updateClipRect(); |
| } |
| |
| private void transformView(float transformationAmount, boolean to, View ownView, |
| View otherView, boolean sameAsAny, boolean useLinearTransformation) { |
| TransformState ownState = TransformState.createFrom(ownView, mTransformInfo); |
| if (useLinearTransformation) { |
| ownState.setDefaultInterpolator(Interpolators.LINEAR); |
| } |
| ownState.setIsSameAsAnyView(sameAsAny); |
| if (to) { |
| if (otherView != null) { |
| TransformState otherState = TransformState.createFrom(otherView, mTransformInfo); |
| ownState.transformViewTo(otherState, transformationAmount); |
| otherState.recycle(); |
| } else { |
| ownState.disappear(transformationAmount, null); |
| } |
| } else { |
| if (otherView != null) { |
| TransformState otherState = TransformState.createFrom(otherView, mTransformInfo); |
| ownState.transformViewFrom(otherState, transformationAmount); |
| otherState.recycle(); |
| } else { |
| ownState.appear(transformationAmount, null); |
| } |
| } |
| ownState.recycle(); |
| } |
| |
| private HashMap<MessagingGroup, MessagingGroup> findPairs(ArrayList<MessagingGroup> ownGroups, |
| ArrayList<MessagingGroup> otherGroups) { |
| mGroupMap.clear(); |
| int lastMatch = Integer.MAX_VALUE; |
| for (int i = ownGroups.size() - 1; i >= 0; i--) { |
| MessagingGroup ownGroup = ownGroups.get(i); |
| MessagingGroup bestMatch = null; |
| int bestCompatibility = 0; |
| for (int j = Math.min(otherGroups.size(), lastMatch) - 1; j >= 0; j--) { |
| MessagingGroup otherGroup = otherGroups.get(j); |
| int compatibility = ownGroup.calculateGroupCompatibility(otherGroup); |
| if (compatibility > bestCompatibility) { |
| bestCompatibility = compatibility; |
| bestMatch = otherGroup; |
| lastMatch = j; |
| } |
| } |
| if (bestMatch != null) { |
| mGroupMap.put(ownGroup, bestMatch); |
| } |
| } |
| return mGroupMap; |
| } |
| |
| private boolean isGone(View view) { |
| if (view.getVisibility() == View.GONE) { |
| return true; |
| } |
| final ViewGroup.LayoutParams lp = view.getLayoutParams(); |
| if (lp instanceof MessagingLinearLayout.LayoutParams |
| && ((MessagingLinearLayout.LayoutParams) lp).hide) { |
| return true; |
| } |
| return false; |
| } |
| |
| @Override |
| public void setVisible(boolean visible, boolean force) { |
| super.setVisible(visible, force); |
| resetTransformedView(); |
| ArrayList<MessagingGroup> ownGroups = mMessagingLayout.getMessagingGroups(); |
| for (int i = 0; i < ownGroups.size(); i++) { |
| MessagingGroup ownGroup = ownGroups.get(i); |
| if (!isGone(ownGroup)) { |
| MessagingLinearLayout ownMessages = ownGroup.getMessageContainer(); |
| for (int j = 0; j < ownMessages.getChildCount(); j++) { |
| View child = ownMessages.getChildAt(j); |
| setVisible(child, visible, force); |
| } |
| setVisible(ownGroup.getAvatar(), visible, force); |
| setVisible(ownGroup.getSenderView(), visible, force); |
| MessagingImageMessage isolatedMessage = ownGroup.getIsolatedMessage(); |
| if (isolatedMessage != null) { |
| setVisible(isolatedMessage, visible, force); |
| } |
| } |
| } |
| } |
| |
| private void setVisible(View child, boolean visible, boolean force) { |
| if (isGone(child) || MessagingPropertyAnimator.isAnimatingAlpha(child)) { |
| return; |
| } |
| TransformState ownState = TransformState.createFrom(child, mTransformInfo); |
| ownState.setVisible(visible, force); |
| ownState.recycle(); |
| } |
| |
| @Override |
| protected void resetTransformedView() { |
| super.resetTransformedView(); |
| ArrayList<MessagingGroup> ownGroups = mMessagingLayout.getMessagingGroups(); |
| for (int i = 0; i < ownGroups.size(); i++) { |
| MessagingGroup ownGroup = ownGroups.get(i); |
| if (!isGone(ownGroup)) { |
| MessagingLinearLayout ownMessages = ownGroup.getMessageContainer(); |
| for (int j = 0; j < ownMessages.getChildCount(); j++) { |
| View child = ownMessages.getChildAt(j); |
| if (isGone(child)) { |
| continue; |
| } |
| resetTransformedView(child); |
| setClippingDeactivated(child, false); |
| } |
| resetTransformedView(ownGroup.getAvatar()); |
| resetTransformedView(ownGroup.getSenderView()); |
| MessagingImageMessage isolatedMessage = ownGroup.getIsolatedMessage(); |
| if (isolatedMessage != null) { |
| resetTransformedView(isolatedMessage); |
| } |
| setClippingDeactivated(ownGroup.getAvatar(), false); |
| setClippingDeactivated(ownGroup.getSenderView(), false); |
| ownGroup.setTranslationY(0); |
| ownGroup.getMessageContainer().setTranslationY(0); |
| ownGroup.getSenderView().setTranslationY(0); |
| } |
| ownGroup.setTransformingImages(false); |
| ownGroup.updateClipRect(); |
| } |
| } |
| |
| @Override |
| public void prepareFadeIn() { |
| super.prepareFadeIn(); |
| setVisible(true /* visible */, false /* force */); |
| } |
| |
| private void resetTransformedView(View child) { |
| TransformState ownState = TransformState.createFrom(child, mTransformInfo); |
| ownState.resetTransformedView(); |
| ownState.recycle(); |
| } |
| |
| @Override |
| protected void reset() { |
| super.reset(); |
| mMessageContainer = null; |
| mMessagingLayout = null; |
| } |
| |
| @Override |
| public void recycle() { |
| super.recycle(); |
| mGroupMap.clear();; |
| sInstancePool.release(this); |
| } |
| } |