Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Animating child view outside of parent

I am trying to animate a view outside of its parent view and when I do the child view is not able to animate beyond its parent. I solved this by using the setClipChildren(false) and it worked... When the view is animated up. When I animate the view down the image is still hidden.

Here is the code that works. This code will animate a tile button to the top of the screen:

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);
        }
    } 

But when I adjust where the view to go to the bottom of the screen, the view below it is "swallowed" and is hidden when it gets to the bottom of the parent view:

 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);
        }
    }

So my question is: How can I animate a child view outside of a parent view below the child view?

UPDATE 1

If I remove the tiles below the tile I am trying to drop, then I am able to see the desired effect until there is another tile below it, at which point the dropping tile will go "behind" the tile that is still there. So how do I get the dropping tile to move over the children below it?

UPDATE 2

One new things I've noticed is that if I move the button left or up, it works fine; but, if move it down or to the right it goes behind the other views. This leads me to believe that buttons created after the current tile have a different effect.

like image 723
BlackHatSamurai Avatar asked May 23 '17 03:05

BlackHatSamurai


2 Answers

Although the answer provided by rupps may solve the problem, but personally I would not use that approach, because:

  • It allocates Bitmap object on the main thread: you should strive not to do that unless in a real need.
  • It unnecessarily * adds boilerplate to the codebase.

* Unnecessarily, because framework provides appropriate API, which is mentioned below.

So, the problem you are trying to solve is to animate a View out of the bounds of it's parent. Let's get acquainted with ViewOverlay API:

An overlay is an extra layer that sits on top of a View (the "host view") which is drawn after all other content in that view (including children, if the view is a ViewGroup). Interaction with the overlay layer is done by adding and removing drawables.

As mentioned by Israel Ferrer Camacho in his "Smoke & Mirrors" talk:

ViewOverlay is gonna be your best friend forever ... in animations.

As an example use cases you can see this:

enter image description here Animating icons using ViewOverlay API. This looks like shared element transition?? Well, that's because Transitions API internally uses ViewOverlay.

Also a nice example by Dave Smith, demonstrating difference between using ViewOverlay and Animator:

In order to complete the answer, I'll post a chunk of code from Dave Smith's example. The usage is as simple as this:

container.getOverlay().add(button);

The button would be "overlayed" atop of container right in the coordinates where it is in the view hierarchy. Now you can perform animations on this button, but the crucial point is to remove the overlay once you do not need it:

@Override
public void onAnimationEnd(Animator arg0) {
    container.getOverlay().remove(button);
}
like image 164
azizbekian Avatar answered Oct 21 '22 08:10

azizbekian


Ok, so in the alternate approach I suggest, I'd use a helper class FlyOverView that takes a "photo" of any view then animates it to whichever desired coordinates. Once the animation is running, you then can hide / delete the original view, as what is moving around the screen is just an image blitted over the canvas. You won't have to worry about clipping other views, etc.

You'll need to declare this FlyOverView on an outer container of your brick system, and that layout has to cover the whole area where you intend the animation to be visible. So I'd suggest to use the following as you root container, it's just a FrameLayout where you'll draw stuff over its children.

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;
            }
        });
    }
}

that you can use as the container

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

          .
          .
    </whatever.GameRootLayout>

Then the FlyOverView itself:

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) {}
    }
}

Then, when you want to animate any view, you just have to call the function in the game root layout:

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

This example also applies alpha and rotation to the view, but you can tune it easily to your needs.

I hope this can inspire you, if you have any question feel free to ask !

like image 39
rupps Avatar answered Oct 21 '22 08:10

rupps