Search code examples
androidandroid-layoutandroid-animationlayout-inflater

Moving inflated Layout/View to a new position in an animated way


I'm inflating a layout to show it on the screen. Now I want to move that layout partially off screen. I tried that using .animate().translationX(-500) on the inflated layout. It moved off screen exactly how I wanted it to look:

Before:

before moving

After:

after moving

But now I have the problem that the area which the layout originally occupied is not clickable (e.g. can't swipe between homescreens, only works outside the blue marked area).

not clickable area

How can I solve that, so blue area is clickable after moving, but the remaing area of the layout (red area) could still register clicks (e.g. if I wanted to move it back in with an onClickListener)? I think I have to work with updateViewLayout() function of the windowManager, but I don't know what I should change regarding the parameters.

Edit 1:

Here is the code. The translation is triggered by the button:

overlay.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#d8ff0000"
    android:weightSum="1">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="245dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceLarge"
            android:text="Lorem ipsum "
            android:id="@+id/textView6"
            android:layout_marginLeft="10dp" />

        <Button
            android:layout_width="55dp"
            android:layout_height="wrap_content"
            android:text="X"
            android:id="@+id/button"
            android:layout_gravity="center_horizontal|top"
            android:layout_alignParentTop="true"
            android:layout_toEndOf="@+id/textView6"
            android:layout_marginStart="27dp" />
    </RelativeLayout>
</LinearLayout>

Service which inflates the layout:

OverlayService.java

public class FloatingMenu extends Service{

    private WindowManager wm;
    private LinearLayout ll;
    private boolean isPushedToSide = true;


    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();

        wm = (WindowManager) getSystemService(WINDOW_SERVICE);
        ll = new LinearLayout(this);


        LinearLayout.LayoutParams llParameters = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
        ll.setBackgroundColor(Color.argb(50, 255, 255, 255));
        ll.setLayoutParams(llParameters);

        final WindowManager.LayoutParams parameters = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.TYPE_PHONE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT);

        parameters.gravity = Gravity.LEFT | Gravity.TOP;

        LayoutInflater li = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
        ll = (LinearLayout) li.inflate(R.layout.overlay, null);
        final Button b = (Button) ll.findViewById(R.id.button);
        b.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (isPushedToSide) {
                    ll.animate().translationX(0);
                    isPushedToSide = false;
                }else {
                    ll.animate().translationX(-450);
                    isPushedToSide = true;
                }
            }
        });
        wm.addView(ll, parameters);
    }
}

Edit 2:

While continuing my research I found out that .animate().translationX() only moves the position where the view is rendered and not the view itself. That also explains why after using .animate().translationX() the onclick event still gets triggered at the same position as before "moving" the layout.

Now I need to find a way to move the actual view to my desired position combining with an animation. Any ideas how to do that?

Edit 3: I found a lot of posts with similar problems and if I'm right the solution to the problem is using ObjectAnimator instead of .animate().translationX(). I tried replacing that part in my code, so that the onClickListener now looks like this:

b.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        if (isPushedToSide) {
            //ll.animate().translationX(0);
            ObjectAnimator.ofFloat(ll, "translationX", 0).setDuration(250).start();
            isPushedToSide = false;
        }else {
            //ll.animate().translationX(-450);
            ObjectAnimator.ofFloat(ll, "translationX", -450).setDuration(250).start();
            isPushedToSide = true;
         }
    }
});

ll is the LinearLayout I have inflated, containing the Lorem ipsum text and Button. Again the animation itself works fine, but behaves still the same way. I still need to click the original position of the button to fire the onClickListener and the blue area is still not clickable.

Edit 4:

Was trying out the suggestion to use.invalidate() to update the actual position. However it didn't work. I'm not sure if used it correct though.

b.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        if (isPushedToSide) {
            //ll.animate().translationX(0);
            ObjectAnimator.ofFloat(ll, "translationX", 0).setDuration(250).start();
            ll.invalidate();
            isPushedToSide = false;
        }else {
            //ll.animate().translationX(-450);
            ObjectAnimator.ofFloat(ll, "translationX", -450).setDuration(250).start();
            ll.invalidate();
            isPushedToSide = true;
         }
    }
});

I also tried changing the x value of LayoutParams and calling wm.updateViewLayou(ll, updatedParameters), which worked fine. When I moved the overlay 100px to the right, the area which triggers onClickListener also gets pushed 100px to the right. The problem is that I was only able to move the overlay within the boundaries of the actual screen and what I need is to move it off screen. Tied negative values, but that didn't work either.

What I essentially need is a drawer navigation but with a limited height (at least that's I think what I need). Gonna see if I can get something to work properly that way.

Edit 5:

Corrected my code so that .inflate() gets called when the animation has finished, but it still behaves the same way:

ObjectAnimator anim = ObjectAnimator.ofFloat(ll, "translationX", 0).setDuration(250);

anim.addListener(new Animator.AnimatorListener() {

    @Override
    public void onAnimationStart(Animator animator) {}

    @Override
    public void onAnimationEnd(Animator animator) {
        b.invalidate();
        text.invalidate();
        frame.invalidate();
        ll.invalidate();
    }

    @Override
    public void onAnimationCancel(Animator animator) {}

    @Override
    public void onAnimationRepeat(Animator animator) {}
});
anim.start();

I also recorded two .gifs showing the problem. I set the gravity to CENTER on the second one. enter image description here enter image description here


Solution

  • I asked the creator of the guide I used for creating such SYSTEM_ALERT_WINDOW windows and he helped me out.

    Here is the code:

    public class Fw  extends Service {
    
        private WindowManager wm;
        private boolean isPushedToSide = true;
    
    
        @Nullable
        @Override
        public IBinder onBind(Intent intent) {
            return null;
        }
    
        WindowManager.LayoutParams parameters;
        Button b;
        View inflating_layout;
        LayoutInflater li;
        ValueAnimator animator;
    
        @Override
    
        public void onCreate() {
            super.onCreate();
    
            wm = (WindowManager) getSystemService(WINDOW_SERVICE);
    
            li = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
            inflating_layout = li.inflate(R.layout.overlay, null);
    
            b = (Button) inflating_layout.findViewById(R.id.button);
    
            parameters = new WindowManager.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.TYPE_PHONE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, PixelFormat.TRANSLUCENT);
            parameters.gravity = Gravity.LEFT | Gravity.TOP;
    
            isPushedToSide = true;
    
            b.setOnClickListener(new View.OnClickListener() {
    
                @Override
                public void onClick(View view) {
                    if (isPushedToSide) {
    
                        isPushedToSide = false;
    
                        animator  = ValueAnimator.ofInt(parameters.x, parameters.x-450);
                        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                            @Override
                            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                                int val = (Integer) valueAnimator.getAnimatedValue();
                                updateView(inflating_layout, val);
    
                            }
                        });
                        animator.setDuration(250);
                        animator.start();
    
                    } else {
    
                        animator = ValueAnimator.ofInt(parameters.x, parameters.x+450);
                        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                            @Override
                            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                                int val = (Integer) valueAnimator.getAnimatedValue();
                                updateView(inflating_layout, val);
    
                            }
                        });
                        animator.setDuration(250);
                        animator.start();
    
    
                        isPushedToSide = true;
                    }
                }
            });
            wm.addView(inflating_layout, parameters);
        }
    
        public void updateView(View view, Integer x) {
            if (view != null) {
                if (x != null) parameters.x = x;
                wm.updateViewLayout(view, parameters);
            }
        }
    }
    

    Now everything seems to work fine. Only the sliding animation is flickering a bit at the border of the screen. With a slower speed e.g. .setDuration(2500) that doesn't happen. I don't know if it's because of the emulator or another reason.