在父视图之外对子视图进行动画处理

5

我尝试在父视图之外动画化一个视图,但是当我这样做时,子视图不能超出其父视图。我解决了这个问题,通过使用setClipChildren(false),它起作用了...... 当视图向上动画时。当我向下动画视图时,图像仍然被隐藏。

这是可行的代码。这段代码将把一个方块按钮动画到屏幕顶部:

private void setGameBoard(){

        brickWall.setClipChildren(false);
        brickWall.setClipToPadding(false);

        //Build game board
        for(int ii = 0; ii < brickRows;ii++){
            final int x = ii;

            //Build table rows
            row = new TableRow(this.getApplicationContext());
            row.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, 50));
            row.setClipChildren(false);
            row.setClipToPadding(false);

           // row.setBackgroundColor(ResourcesCompat.getColor(getResources(), R.color.colorAccent, null));

            //Build table tiles
            for(int jj=0; jj < brickColumns; jj++){
                final int y = jj;
                final Brick tile = new Brick(this);
                tile.setClipBounds(null);

                tile.setBackgroundColor(ResourcesCompat.getColor(getResources(), R.color.colorPrimary, null));

                //Set margins to create look of tic-tac-toe
                TableRow.LayoutParams lp = new TableRow.LayoutParams(
                                           150, 75);
                lp.setMargins(0,0,0,0);


                //lp.weight = 1;
                tile.setLayoutParams(lp);

                tile.setWidth(3);
                tile.setHeight(10);
                tile.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if(tile.getHits() == 0){
                            tile.setBackgroundColor(ResourcesCompat.getColor(getResources(),
                                    R.color.colorGreen, null));
                            tile.addHit();
                        } else if (tile.getHits() == 1){
                            tile.setBackgroundColor(ResourcesCompat.getColor(getResources(),
                                    R.color.colorYellow, null));
                            tile.addHit();
                        }else if(tile.getHits() == 2){
                            brokenBricks++;

                            float bottomOfScreen = getResources().getDisplayMetrics()
                                    .heightPixels;

                            ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(tile, "translationY",
                                    -2000);
// 2
                            objectAnimator.setDuration(2000);
                            tile.setHapticFeedbackEnabled(true);
                            objectAnimator.start();
                            //tile.setVisibility(View.INVISIBLE);
                            if(isRevealComplete()){
                                brickWall.setVisibility(View.INVISIBLE);
                            }
                        }
                    }
                });

                row.addView(tile);
            }

            brickWall.addView(row);
        }
    } 

但是当我调整视图到屏幕底部时,下面的视图被“吞没”,当它到达父视图的底部时就会被隐藏:

 private void setGameBoard(){

        brickWall.setClipChildren(false);
        brickWall.setClipToPadding(false);

        //Build game board
        for(int ii = 0; ii < brickRows;ii++){
            final int x = ii;

            //Build table rows
            row = new TableRow(this.getApplicationContext());
            row.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, 50));
            row.setClipChildren(false);
            row.setClipToPadding(false);

           // row.setBackgroundColor(ResourcesCompat.getColor(getResources(), R.color.colorAccent, null));

            //Build table tiles
            for(int jj=0; jj < brickColumns; jj++){
                final int y = jj;
                final Brick tile = new Brick(this);
                tile.setClipBounds(null);

                tile.setBackgroundColor(ResourcesCompat.getColor(getResources(), R.color.colorPrimary, null));

                //Set margins to create look of tic-tac-toe
                TableRow.LayoutParams lp = new TableRow.LayoutParams(
                                           150, 75);
                lp.setMargins(0,0,0,0);


                //lp.weight = 1;
                tile.setLayoutParams(lp);

                tile.setWidth(3);
                tile.setHeight(10);
                tile.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if(tile.getHits() == 0){
                            tile.setBackgroundColor(ResourcesCompat.getColor(getResources(),
                                    R.color.colorGreen, null));
                            tile.addHit();
                        } else if (tile.getHits() == 1){
                            tile.setBackgroundColor(ResourcesCompat.getColor(getResources(),
                                    R.color.colorYellow, null));
                            tile.addHit();
                        }else if(tile.getHits() == 2){
                            brokenBricks++;

                            float bottomOfScreen = getResources().getDisplayMetrics()
                                    .heightPixels;

                            ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(tile, "translationY",
                                    2000);
                            objectAnimator.setDuration(2000);
                            tile.setHapticFeedbackEnabled(true);
                            objectAnimator.start();
                            //tile.setVisibility(View.INVISIBLE);
                            if(isRevealComplete()){
                                brickWall.setVisibility(View.INVISIBLE);
                            }
                        }
                    }
                });

                row.addView(tile);
            }

            brickWall.addView(row);
        }
    }

所以我的问题是:如何在子视图下方的父视图之外动画显示子视图? 更新1 如果我移除正在尝试放置的瓷砖下面的瓷砖,那么我就能看到期望的效果,直到它下面有另一个瓷砖,此时掉落的瓷砖将“隐藏”在仍然存在的瓷砖后面。那么,我该如何使掉落的瓷砖移动到其下方的子元素上方? 更新2 我注意到的一件新事情是,如果我将按钮向左或向上移动,它就可以正常工作;但是,如果将其向下或向右移动,它会隐藏在其他视图后面。这让我相信,在当前瓷砖之后创建的按钮具有不同的效果。

作为一种极端的测试技巧,你可以给动画视图添加一些高度(1dp)吗?高度会改变视图的Z顺序,因此我怀疑这可能会对你的问题产生影响。这可能是一个解决方法,但根本问题需要更深入的调查! - rupps
我已经尝试过那样做了,但没有任何影响。 - BlackHatSamurai
此外,您是否考虑使用轻量级的FrameLayout,并仅使用正确的间距放置瓷砖?这将消除嵌套布局并使整体结果结构更轻。我会采取这种方式,但我不知道您的应用程序的具体情况,在我的经验中,setClipChildren()在布局越平坦时效果最佳。 - rupps
此外,您可以将 setClipChildren(false) 设置到包含 brickWall 的布局中吗?如果其高度为 wrap_content,则可能会解释您的底部剪切问题。 - rupps
我认为发生的情况是,该按钮之后创建的瓷砖具有更高的z-order。我过去实现过一些在这些情况下非常有效的东西,即通过视图及其内容创建位图,隐藏视图,然后在dispatchDraw()中动画化这个“幽灵图像”,在那里您可以绝对控制绘制所有内容,或将其作为另一个视图添加到其他内容上方。如果您感兴趣,我可以提供更多详细信息。 - rupps
显示剩余2条评论
2个回答

11
尽管rupps提供的answer可以解决问题,但个人不会采用该方法,因为:
  • 它在主线程上分配Bitmap对象:除非确有需要,否则应努力避免这样做。
  • 它不必要地向代码库添加了样板文件。

* 不必要,因为框架提供了适当的API,下面会提到。

所以,您要解决的问题是将一个View动画移出其父级范围。 让我们熟悉ViewOverlay API:

覆盖层是位于视图(“主机视图”)之上的额外层,在该视图中的所有其他内容之后绘制(包括子项,如果该视图是ViewGroup)。 通过添加和删除可绘制对象与叠加层进行交互。

正如Israel Ferrer Camacho在他的"Smoke & Mirrors" talk中所提到的:

ViewOverlay 在动画中将成为您永远的好朋友...

您可以查看此处作为示例用途:

enter image description here 使用 ViewOverlay API 动画图标。这看起来像共享元素转换? 这是因为转换 API在内部使用ViewOverlay

同时也有一个很好的例子(链接1),由Dave Smith提供,演示了使用ViewOverlayAnimator之间的差异:

为了完整回答,我会发布 Dave Smith 示例代码的一部分。使用方法就像这样简单:
container.getOverlay().add(button);
将被覆盖在容器的坐标系中,以便于在视图层次结构中进行动画,但是关键点在于在不需要它时要删除覆盖内容。请保留HTML标签。
@Override
public void onAnimationEnd(Animator arg0) {
    container.getOverlay().remove(button);
}

是的,这个OverlayView是我写那段代码之后添加的,顺便说一下,它基本上做了同样的事情(如果你看一下源代码的话),只不过我要说,我的解决方案更加直截了当,因为API考虑到了许多在OP案例中不适用的边缘情况。关于主线程上的位图,我只是使用了一个小的View位图缓存,没有加载或阻塞任何内容,并且该位图没有进行解码而是本地复制。在这种情况下创建一个线程的开销会更大。 - rupps
我不倾向于声称,你的实现比我提供的实现更差。这只是另一种角度和另一种实现方式,可能在某些方面更好或更差。尽管如此,如果我要实现这样的功能,我会完全使用平台提供的API(正如你所提到的,处理所有边缘情况),而不是自己重新发明轮子。 - azizbekian
@BlackHatSamurai,我不明白你的意思。你能否澄清或重新表述你的问题? - azizbekian
我对此并不确定,@rupps。因为当我删除动画代码时,正确的砖块被移除了。更具体地说,当我删除覆盖层代码时。 - BlackHatSamurai
这是新的问题@rupps: https://stackoverflow.com/questions/44209407/viewoverlay-code-is-remving-last-view-in-row - BlackHatSamurai
显示剩余9条评论

4

好的,那么在我建议的另一种方法中,我会使用一个帮助类FlyOverView来拍摄任何视图的“照片”,然后将其动画化到所需坐标。一旦动画运行,你就可以隐藏/删除原始视图,因为在屏幕上移动的只是覆盖画布的图像。你不必担心剪辑其他视图等问题。

你需要在你的积木系统的外部容器中声明这个FlyOverView,并且该布局必须覆盖你打算让动画可见的整个区域。因此,我建议使用以下作为你的根容器,它只是一个FrameLayout,你可以在它的子项上绘制东西。

package whatever;

public class GameRootLayout extends FrameLayout {

    // the currently-animating effect, if any
    private FlyOverView mFlyOverView;

    public GameRootLayout(Context context, AttributeSet attrs) {
         super(context,attrs);
    }

    @Override
    public void dispatchDraw(Canvas canvas) {

        // draw the children            
        super.dispatchDraw(canvas);

        // draw our stuff
        if (mFlyOverView != null) {
            mFlyOverView.delegate_draw(canvas);
        }
    }


    /**
     * Starts a flyover animation for the specified view. 
     * It will "fly" to the desired position with an alpha / translation / rotation effect
     *
     * @param viewToFly The view to fly
     * @param targetX The target X coordinate
     * @param targetY The target Y coordinate
     */

    public void addFlyOver(View viewToFly, @Px int targetX, @Px int targetY) {
        if (mFlyOverView != null) mFlyOverView.cancel();
        mFlyOverView = new FlyOverView(this, viewToFly, targetX, targetY, new FlyOverView.OnFlyOverFinishedListener() {
            @Override
            public void onFlyOverFinishedListener() {
                mFlyOverView = null;
            }
        });
    }
}

你可以用作容器的东西

  <whatever.GameRootLayout
          android:id="@+id/gameRootLayout"
          android:layout_width="match_parent"
          android:layout_height="match_parent">

          .
          .
    </whatever.GameRootLayout>

然后是 FlyOverView 本身:

package com.regaliz.gui.fx;

import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.util.Log;
import android.view.View;
import android.support.annotation.Px;
import android.view.animation.AccelerateDecelerateInterpolator;


/**
 * Neat animation that captures a layer bitmap and "flies" it to the corner of the screen, used to create
 * an "added to playlist" effect or something like that. The method delegate_draw() must be called from the
 * parent view to update the animation
 *
 * @author rupps'2014
 * license public domain, attribution appreciated
 */

@SuppressWarnings("unused")
public class FlyOverView {

    public  static final int DEFAULT_DURATION = 1000;
    private static final String TAG = "FlyOverView";
    private static final boolean LOG_ON = false;

    private ObjectAnimator mFlyoverAnimation;

    private float mCurrentX, mCurrentY;
    private Matrix mMatrix = new Matrix();

    private Bitmap mBitmap;
    private Paint mPaint = new Paint();

    private View mParentView = null;
    private OnFlyOverFinishedListener mOnFlyOverFinishedListener;

    public interface OnFlyOverFinishedListener {
        void onFlyOverFinishedListener();
    }

    /**
     * Creates the FlyOver effect
     *
     * @param parent    Container View to invalidate. That view has to call this class' delegate_draw() in its dispatchDraw().
     * @param viewToFly Target View to animate
     * @param finalX    Final X coordinate
     * @param finalY    Final Y coordinate
     */

    public FlyOverView(View parent, View viewToFly, @Px int finalX, @Px int finalY, OnFlyOverFinishedListener listener) {
        setupFlyOver(parent, viewToFly, finalX, finalY, DEFAULT_DURATION, listener);
    }

    /**
     * Creates the FlyOver effect
     *
     * @param parent    Container View to invalidate. That view has to call this class' delegate_draw() in its dispatchDraw().
     * @param viewToFly Target View to animate
     * @param finalX    Final X coordinate
     * @param finalY    Final Y coordinate
     * @param duration  Animation duration
     */

    public FlyOverView(View parent, View viewToFly, @Px int finalX, @Px int finalY, int duration, OnFlyOverFinishedListener listener) {
        setupFlyOver(parent, viewToFly, finalX, finalY, duration, listener);
    }

    /**
     * cancels current animation from the outside
     */
    public void cancel() {
        if (mFlyoverAnimation != null) mFlyoverAnimation.cancel();
    }

    private void setupFlyOver(View parentContainer, View viewToFly, @Px int finalX, @Px int finalY, int duration, OnFlyOverFinishedListener listener) {


        int[] location = new int[2];

        mParentView = parentContainer;
        mOnFlyOverFinishedListener = listener;
        viewToFly.getLocationInWindow(location);

        int 
            sourceX = location[0],
            sourceY = location[1];

        if (LOG_ON) Log.v(TAG, "FlyOverView, item " + viewToFly+", finals "+finalX+", "+finalY+", sources "+sourceX+", "+sourceY+ " duration "+duration);

         /* Animation definition table */

        mFlyoverAnimation = ObjectAnimator.ofPropertyValuesHolder(
            this,
            PropertyValuesHolder.ofFloat("translationX", sourceX, finalX),
            PropertyValuesHolder.ofFloat("translationY", sourceY, finalY),
            PropertyValuesHolder.ofFloat("scaleAlpha", 1, 0.2f) // not to 0 so we see the end of the effect in other properties
        );

        mFlyoverAnimation.setDuration(duration);
        mFlyoverAnimation.setRepeatCount(0);
        mFlyoverAnimation.setInterpolator(new AccelerateDecelerateInterpolator());
        mFlyoverAnimation.addListener(new SimpleAnimationListener() {
            @Override
            public void onAnimationEnd(Animator animation) {
                if (LOG_ON) Log.v(TAG, "FlyOver: End");
                mParentView.invalidate();
                if (mBitmap != null) mBitmap.recycle(); // just for safety
                mBitmap = null;
                mOnFlyOverFinishedListener.onFlyOverFinishedListener();
            }
        });

        // take snapshot of viewToFly
        viewToFly.setDrawingCacheEnabled(true);
        mBitmap = Bitmap.createBitmap(viewToFly.getDrawingCache());
        viewToFly.setDrawingCacheEnabled(false);

        mFlyoverAnimation.start();

    }

    // ANIMATOR setter
    public void setTranslationX(float position) {
        mCurrentX = position;
    }

    // ANIMATOR setter
    public void setTranslationY(float position) {
        mCurrentY = position;
    }

    // ANIMATOR setter
    // as this will be called in every iteration, we set here all parameters at once then call invalidate,
    // rather than separately
    public void setScaleAlpha(float position) {

        mPaint.setAlpha((int) (100 * position));
        mMatrix.setScale(position, position);
        mMatrix.postRotate(360 * position); // asemos de to'
        mMatrix.postTranslate(mCurrentX, mCurrentY);

        mParentView.invalidate();
    }

    /**
     * This has to be called from the root container's dispatchDraw()
     * in order to update the animation.
     */

    public void delegate_draw(Canvas c) {
        if (LOG_ON) Log.v(TAG, "CX " + mCurrentX + ", CY " + mCurrentY);
        c.drawBitmap(mBitmap, mMatrix, mPaint);
    }

    private abstract class SimpleAnimationListener implements Animator.AnimatorListener {
        @Override public void onAnimationStart(Animator animation) {}
        @Override public void onAnimationRepeat(Animator animation) {}
        @Override public void onAnimationCancel(Animator animation) {}
    }
}

然后,当您想要为任何视图添加动画时,只需在游戏根布局中调用该函数:

GameRootLayout rootLayout = (GameRootLayout)findViewById(...);
.
.
rootLayout.addFlyOver(yourBrick, targetX, targetY);

这个例子也将 alpha 和旋转应用于视图,但您可以轻松地根据自己的需求进行调整。

我希望这能给您带来灵感,如果您有任何问题,请随时问!


所以这个功能非常好。需要稍微调整一下动画,我不喜欢它在页面底部变小,但除此之外,它的表现非常好。 - BlackHatSamurai
很高兴你喜欢它,是的,这有点夸张,我在媒体播放器上使用它,其中缩略图很大,所以效果是它们被顶角“吞噬”。要根据您的需求更改它,只需在 setScaleAlpha 中更改位置范围从1.0到0.0递减即可。 - rupps

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接