Add support for delete animation in LayoutAnimation on Android

Summary:
Android follow up to #6779

**Test plan**
Tested add/removing views in the UIExample explorer with and without setting a LayoutAnimation. Tested that user interation during the animation is properly disabled.

![layout-anim-2](https://cloud.githubusercontent.com/assets/2677334/14760549/d60ebe2a-0914-11e6-8f17-ea04d8bf813b.gif)
Closes https://github.com/facebook/react-native/pull/7171

Differential Revision: D3352450

Pulled By: astreet

fbshipit-source-id: 233efa041626eb26d99511d12a924e54a10f96cc
This commit is contained in:
Janic Duplessis
2016-05-26 05:20:51 -07:00
committed by Facebook Github Bot 9
parent 4879f88a75
commit 0fb5ccf6af
7 changed files with 138 additions and 10 deletions

View File

@@ -37,9 +37,12 @@ const UIExplorerExampleList = require('./UIExplorerExampleList');
const UIExplorerList = require('./UIExplorerList');
const UIExplorerNavigationReducer = require('./UIExplorerNavigationReducer');
const UIExplorerStateTitleMap = require('./UIExplorerStateTitleMap');
const UIManager = require('UIManager');
const URIActionMap = require('./URIActionMap');
const View = require('View');
UIManager.setLayoutAnimationEnabledExperimental(true);
const DRAWER_WIDTH_LEFT = 56;
type Props = {

View File

@@ -35,6 +35,7 @@ import com.facebook.react.bridge.SoftAssertions;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.touch.JSResponderHandler;
import com.facebook.react.uimanager.layoutanimation.LayoutAnimationController;
import com.facebook.react.uimanager.layoutanimation.LayoutAnimationListener;
import com.facebook.systrace.Systrace;
import com.facebook.systrace.SystraceMessage;
@@ -294,8 +295,8 @@ public class NativeViewHierarchyManager {
@Nullable int[] indicesToRemove,
@Nullable ViewAtIndex[] viewsToAdd,
@Nullable int[] tagsToDelete) {
ViewGroup viewToManage = (ViewGroup) mTagsToViews.get(tag);
ViewGroupManager viewManager = (ViewGroupManager) resolveViewManager(tag);
final ViewGroup viewToManage = (ViewGroup) mTagsToViews.get(tag);
final ViewGroupManager viewManager = (ViewGroupManager) resolveViewManager(tag);
if (viewToManage == null) {
throw new IllegalViewOperationException("Trying to manageChildren view with tag " + tag +
" which doesn't exist\n detail: " +
@@ -344,7 +345,17 @@ public class NativeViewHierarchyManager {
viewsToAdd,
tagsToDelete));
}
viewManager.removeViewAt(viewToManage, indicesToRemove[i]);
View viewToRemove = viewToManage.getChildAt(indexToRemove);
if (mLayoutAnimator.shouldAnimateLayout(viewToRemove) &&
arrayContains(tagsToDelete, viewToRemove.getId())) {
// The view will be removed and dropped by the 'delete' layout animation
// instead, so do nothing
} else {
viewManager.removeViewAt(viewToManage, indexToRemove);
}
lastIndexToRemove = indexToRemove;
}
}
@@ -371,7 +382,7 @@ public class NativeViewHierarchyManager {
if (tagsToDelete != null) {
for (int i = 0; i < tagsToDelete.length; i++) {
int tagToDelete = tagsToDelete[i];
View viewToDestroy = mTagsToViews.get(tagToDelete);
final View viewToDestroy = mTagsToViews.get(tagToDelete);
if (viewToDestroy == null) {
throw new IllegalViewOperationException(
"Trying to destroy unknown view tag: "
@@ -383,11 +394,31 @@ public class NativeViewHierarchyManager {
viewsToAdd,
tagsToDelete));
}
dropView(viewToDestroy);
if (mLayoutAnimator.shouldAnimateLayout(viewToDestroy)) {
mLayoutAnimator.deleteView(viewToDestroy, new LayoutAnimationListener() {
@Override
public void onAnimationEnd() {
viewManager.removeView(viewToManage, viewToDestroy);
dropView(viewToDestroy);
}
});
} else {
dropView(viewToDestroy);
}
}
}
}
private boolean arrayContains(int[] array, int ele) {
for (int curEle : array) {
if (curEle == ele) {
return true;
}
}
return false;
}
/**
* Simplified version of constructManageChildrenErrorMessage that only deals with adding children
* views

View File

@@ -48,6 +48,15 @@ public abstract class ViewGroupManager <T extends ViewGroup>
parent.removeViewAt(index);
}
public void removeView(T parent, View view) {
for (int i = 0; i < getChildCount(parent); i++) {
if (getChildAt(parent, i) == view) {
removeViewAt(parent, i);
break;
}
}
}
public void removeAllViews(T parent) {
for (int i = getChildCount(parent) - 1; i >= 0; i--) {
removeViewAt(parent, i);

View File

@@ -6,6 +6,7 @@ import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import com.facebook.react.bridge.ReadableMap;
@@ -25,6 +26,7 @@ public class LayoutAnimationController {
private final AbstractLayoutAnimation mLayoutCreateAnimation = new LayoutCreateAnimation();
private final AbstractLayoutAnimation mLayoutUpdateAnimation = new LayoutUpdateAnimation();
private final AbstractLayoutAnimation mLayoutDeleteAnimation = new LayoutDeleteAnimation();
private boolean mShouldAnimateLayout;
public void initializeFromConfig(final @Nullable ReadableMap config) {
@@ -49,11 +51,17 @@ public class LayoutAnimationController {
config.getMap(LayoutAnimationType.UPDATE.toString()), globalDuration);
mShouldAnimateLayout = true;
}
if (config.hasKey(LayoutAnimationType.DELETE.toString())) {
mLayoutDeleteAnimation.initializeFromConfig(
config.getMap(LayoutAnimationType.DELETE.toString()), globalDuration);
mShouldAnimateLayout = true;
}
}
public void reset() {
mLayoutCreateAnimation.reset();
mLayoutUpdateAnimation.reset();
mLayoutDeleteAnimation.reset();
mShouldAnimateLayout = false;
}
@@ -65,7 +73,8 @@ public class LayoutAnimationController {
/**
* Update layout of given view, via immediate update or animation depending on the current batch
* layout animation configuration supplied during initialization.
* layout animation configuration supplied during initialization. Handles create and update
* animations.
*
* @param view the view to update layout of
* @param x the new X position for the view
@@ -76,9 +85,9 @@ public class LayoutAnimationController {
public void applyLayoutUpdate(View view, int x, int y, int width, int height) {
UiThreadUtil.assertOnUiThread();
// Determine which animation to use : if view is initially invisible, use create animation.
// If view is becoming invisible, use delete animation. Otherwise, use update animation.
// This approach is easier than maintaining a list of tags for recently created/deleted views.
// Determine which animation to use : if view is initially invisible, use create animation,
// otherwise use update animation. This approach is easier than maintaining a list of tags
// for recently created views.
AbstractLayoutAnimation layoutAnimation = (view.getWidth() == 0 || view.getHeight() == 0) ?
mLayoutCreateAnimation :
mLayoutUpdateAnimation;
@@ -91,4 +100,54 @@ public class LayoutAnimationController {
view.startAnimation(animation);
}
}
/**
* Animate a view deletion using the layout animation configuration supplied during initialization.
*
* @param view The view to animate.
* @param listener Called once the animation is finished, should be used to
* completely remove the view.
*/
public void deleteView(final View view, final LayoutAnimationListener listener) {
UiThreadUtil.assertOnUiThread();
AbstractLayoutAnimation layoutAnimation = mLayoutDeleteAnimation;
Animation animation = layoutAnimation.createAnimation(
view, view.getLeft(), view.getTop(), view.getWidth(), view.getHeight());
if (animation != null) {
disableUserInteractions(view);
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation anim) {}
@Override
public void onAnimationRepeat(Animation anim) {}
@Override
public void onAnimationEnd(Animation anim) {
listener.onAnimationEnd();
}
});
view.startAnimation(animation);
} else {
listener.onAnimationEnd();
}
}
/**
* Disables user interactions for a view and all it's subviews.
*/
private void disableUserInteractions(View view) {
view.setClickable(false);
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup)view;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
disableUserInteractions(viewGroup.getChildAt(i));
}
}
}
}

View File

@@ -0,0 +1,10 @@
// Copyright 2004-present Facebook. All Rights Reserved.
package com.facebook.react.uimanager.layoutanimation;
/**
* Listener invoked when a layout animation has completed.
*/
public interface LayoutAnimationListener {
void onAnimationEnd();
}

View File

@@ -7,7 +7,8 @@ package com.facebook.react.uimanager.layoutanimation;
*/
/* package */ enum LayoutAnimationType {
CREATE("create"),
UPDATE("update");
UPDATE("update"),
DELETE("delete");
private final String mName;

View File

@@ -0,0 +1,15 @@
// Copyright 2004-present Facebook. All Rights Reserved.
package com.facebook.react.uimanager.layoutanimation;
/**
* Class responsible for handling layout view deletion animation, applied to view whenever a
* valid config was supplied for the layout animation of DELETE type.
*/
/* package */ class LayoutDeleteAnimation extends BaseLayoutAnimation {
@Override
boolean isReverse() {
return true;
}
}