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.
Although the answer provided by rupps may solve the problem, but personally I would not use that approach, because:
Bitmap
object on the main thread: you should strive not to do that unless in a real need.* 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:
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);
}
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 !
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With