I have started working on an app that uses an overlay service to display a utility sidebar on the phone.
What I did was to add a small arrow handle to the right side of the screen, and when the user swipes over it, the sidebar shows up.
This worked without issue up to Jelly Bean. Starting with JB, the entire process of showing the sidebar started to become animated. Which would be very cool, if it were not animating all over the place.
What I did to show and hide the sidebar was a simple updateViewLayout command as below.
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
handlesize, WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
PixelFormat.TRANSLUCENT);
params.gravity = Gravity.RIGHT|Gravity.CENTER_VERTICAL;
wm.updateViewLayout(mOverlay, params);
handleButton.setImageResource(R.drawable.handle);
isCompact=true;
This piece of code hides the sidebar. The handlesize variable is the width of the handle. The wm variable is the window manager. So basically, what I do is only show the width of the handle, so anything else that might be next to the handle(the sidebar is not visible). I also use WRAP_CONTENT for the height, to make sure the only part that absorbs touch events is the area used by the handle, so as to not block off the entire right hand side of the screen from registering touches.
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
PixelFormat.TRANSLUCENT);
params.gravity = Gravity.RIGHT|Gravity.CENTER_VERTICAL;
wm.updateViewLayout(mOverlay, params);
handleButton.setImageResource(R.drawable.handle_back);
isCompact=false;
This is the code that displays the sidebar. The width of the whole view is set to WRAP_CONTENT, so it shows all the content, and the height is set to MATCH_PARENT, because the sidebar is full height when it's visible.
Now normally, this code should instantly switch between one width and height set to the other width and height set, nothing in between. But since Jelly Bean, this has started doing this through some weird animation. I'm not sure, but it may have something to do with Project Butter.
In the link below, you can see some frames of the animation.
http://i.stack.imgur.com/QzZGO.png
If I set the height of the view, even when closed, to MATCH_PARENT, then the animation is just on the horizontal plane, so right to left, which does look nice. The problem with that is I don't want to block the entire right hand side of the screen. There are a lot of uses for sliding from the right in other apps that might run under the service, like switching tabs in Chrome.
So my question is, do you have any idea what I should do to stop this behavior, or, even better, to use it to my advantage in order to make the sidebar do some smooth animations, but in a way that works on Android 2.2 and above?
Thanks a lot!
Since JB Android has added window animations to layout changes. If you disable window animation in Developer settings, the behavior will stop. Also, you cannot disable/enable this programmatically (requires system permission if I remember correctly), so the solution is to remove the view completely and attach it again. Instead of wm.updateViewLayout(view,params) you have to wm.removeView(view) and then wm.addView(view,params).
Related
I have a fairly complicated situation where I need to either process events in a custom view, which is added via WindowManager, or pass them to the underlying window if it is outside of the wanted area. The wanted area is the containerView where it can be smaller from the root view itself, or may have equal width / height.
The view has a size of 28x28, but it can grow up until 60x60. The growing part is done with ValueAnimator where current width and target width is determined by the ValueAnimator.getAnimatedValue() (in this case, between 28 and 60). The window needs to consume the event if it has been clicked on, or the target view which may be smaller than the window itself, is clicked.
An example of the layout looks like this:
<FrameLayout android:layout_width="wrap_content"
android:layout_height="wrap_content">
<FrameLayout android:id="#+id/containerView"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_gravity="center">
<!-- rest of the view, not important -->
<!-- the containerView can have 28x28 size or
60x60 size -->
</FrameLayout>
</FrameLayout>
The animated view is the one that is defined with android:id="#+id/containerView".
I've tried to attach the view using regular layout params, like this, to make the window layout dynamic:
WindowManager manager = context.getSystemService(WindowManager.class);
View rootView = LayoutInflater.from(context).inflate(resId, null, false);
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
params.flags = FLAG_NOT_FOCUSABLE | FLAG_WATCH_OUTSIDE_TOUCH;
manager.addView(rootView, params);
And this similar code block adds the view with 28x28 size, that's not a problem. However, while animating to 60x60 size depending on a state change (on containerView), the animation flickers a lot. I guess it happens because both view itself and the window needs to be re-sized. I've tried to use setLayerType(HARDWARE, null) but that didn't seem to work. Then I've found another workaround, which is increasing the size of the window directly, before starting the animation, by giving it fixed width-height values, like this:
params.width = dpToPx(60);
params.height = dpToPx(60);
manager.updateViewLayout(rootView, params);
And after this, I start the growing animation, which changes the containerView width and height gradually. With this way, animation is smooth, even on lower-end devices so I think it's a good optimization.
The problem begins with the window size change. You see, containerView has to have the attribute android:layout_gravity="center" to position the view to window's center. But, increasing the window width and height changes the view's position. To overcome that, I've decided to write another approach by doing something like this:
// This method is inside the root view, which contains
// the WindowManager.LayoutParams as its layout params.
private void setWindowSize(int widthPx, int heightPx)
{
WindowManager.LayoutParams params = getLayoutParams(); // ignore cast
int oldWidth = params.width;
int oldHeight = params.height;
int differenceWidth = widthPx - oldWidth;
int differenceHeight = heightPx - oldHeight;
// Position the view relatively to the window so
// it should look like its position is not changed
// due to containerView's center layout_gravity.
params.x -= differenceWidth / 2;
params.y -= differenceHeight / 2;
params.width = widthPx;
params.height = heightPx;
// Update itself since this is already the root view.
manager.updateViewLayout(this, params);
}
The code above was causing the position change happening with animation. Hence, I've searched if this animation can be disabled, and found an answer here which seems to be working with Android 10 emulator. However, I don't think this is a reliable approach, as most manufacturers change source codes of framework classes to implement their own themes etc. so I'm looking for a more reliable approach. The change also cause a flicker due to the containerView.onLayout() operation, presumably happening after manager.updateViewLayout() is executed, where it appears on top-left for one frame and on center on the 2nd frame, visible to the eyes.
At this point, I can only think of some ways to prevent these bugs:
1) Process touch events only on certain states (such as the coordinates intercepting the containerView)
2) Make the view non-touchable after receiving MotionEvent.ACTION_OUTSIDE which will indicate a touch event happened outside of the view's boundaries.
1st one has a flaw: If the view is clickable in all cases, it becomes clickable starting from the root view, and once the touch event is received from that view, it is not transferred to other windows (a.k.a underlying applications) which cause an issue.
2nd one seemed a good approach for me, but the event MotionEvent.ACTION_OUTSIDE does not contain any specific x or y coordinates so it is impossible to tell if the event occurred in window's boundaries. If this was possible, I'd add FLAG_NOT_TOUCHABLE to layout params and updated the view, and removed that flag if the touch is to be processed.
So, my question is:
Can a custom view, that has been added with a WindowManager choose to deliver the events further based on, i don't know, returning false from dispatchTouchEvent() or something? Or, is there a way to receive all touch events even outside our application with the specific screen coordinates so I can change the window flags depending on it?
Any help is appreciated, thank you very much.
I was able to resolve the issue by applying an ugly hack. I've used a second window, where the window itself is full screen and contains flags FLAG_NOT_FOCUSABLE and FLAG_NOT_TOUCHABLE and since the touch is disabled the events are passed below window.
The window-resize flickering depending on the animation was the cause, so I've thought about using a temporary view, added the view to a second window, by getting the cache of the view itself using a bitmap and a canvas (the states are cached and recycled by the way), and making the image view visible, setting the view on the original window as INVISIBLE and after making sure it became invisible (by using ViewTreeObserver.addOnDrawListener because the draw function is called) changing window size.
With this approach, the view becomes already invisible while the window size is changed, and translated accordingly, which eliminated the possibility of the buggy view.
Then, after the layout is complete (I've also made sure by using ViewTreeObserver.addOnGlobalLayoutListener() and waiting for the view to be placed on the target coordinates relative to parent), switched the views. Extra memory is used because of the extra added window and image view and bitmap, but the issue seems to be resolved.
The only remaining thing is how to disable window animations with the call windowManager.updateViewLayout() because the flag that the other question mentioned is apparently added in API 18, whereas this app targets to API 16. On the rest of the emulators and the devices that I've tested on seem to have this flag consistently, and the window translate animations seem to be disabled successfully.
I am making an app that creates tiny sprite animations that walk around your screen.
I have a main activity, with a button "start service". This starts a service, which (in onCreate()) creates a full-screen view and attaches it to the root window manager.
This part works perfectly. It fills the screen, and you can leave the app, and the animations will still be visible over everything.
The problem emerges when you rotate the device.
Sprites have moved to the middle of the screen, but this is an unrelated issue; the important thing here is the dark bounding box — this box shows you the canvas I am allowed to draw into, and it needs to fill the whole screen
The view is still portrait-mode, even though all other layouts seem to have updated correctly.
Part of the problem comes from how I have specified the dimensions of the view. I used flags to specify "full screen", but this did not set the view's width and height to match the screen's dimensions. Thus I had to set those manually at startup.
I don't know a way to update the width and height of the view once the view is created, but I feel that I need to, since the dimensions of the view determine the dimensions of the canvas.
My service creates the view like so:
public class WalkerService extends Service {
private WalkerView view;
#Override
public void onCreate() {
super.onCreate();
final WindowManager wm = (WindowManager)getSystemService(WINDOW_SERVICE);
final DisplayMetrics metrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(metrics);
view = new WalkerView(this); // extends SurfaceView
final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY, // TYPE_SYSTEM_ALERT is denied in apiLevel >=19
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
PixelFormat.TRANSLUCENT
);
view.setFitsSystemWindows(false); // allow us to draw over status bar, navigation bar
// here I _manually_ set the dimensions, because otherwise it defaults to a square (something like 1024x1024)
params.width = metrics.widthPixels;
params.height = metrics.heightPixels;
wm.addView(view, params);
}
}
I have tried listening for orientation changes, and rotating the view when that happens:
#Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
view.setRotation(
newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE
? 90.0f
: 0.0f
);
}
But setRotation() didn't seem to affect width and height of my view, nor did they change the angle at which the canvas was rendered.
Am I fundamentally misunderstanding how to create a full-screen overlay? How can I maintain the ability to draw over the entire screen, regardless of orientation (and even when my app is not the active app)?
Maybe it's related to the fact that I attach my view to the Window Service's window manager — perhaps this means my view has an odd parent (I could imagine that maybe the root view would not be affected by orientation, or would have no defined boundaries for children to stretch to fill).
Full source code is public on GitHub in case I've omitted any useful information here.
WalkerView.java may be particularly relevant, so here it is.
I was misled by the answer to a previous question "How to create always-top fullscreen overlay activity in Android". Maybe it worked for an older API level? I'm using API level 24.
That particular answer had recommended:
final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
PixelFormat.TRANSLUCENT
);
There is a problem with this. The constructors that exist for WindowManager.LayoutParams are as follows:
So the WindowManager.LayoutParams.FLAG_FULLSCREEN flag gets used as an explicit value for int w and int h. This is no good!
I found that the correct construction for a full-screen overlay is like so:
final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY, // TYPE_SYSTEM_ALERT is denied in apiLevel >=19
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_FULLSCREEN,
PixelFormat.TRANSLUCENT
);
This means we no longer explicitly specify a width and height. The layout relies entirely on our flags instead.
Yes, WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN is a required flag still; it is necessary if you want to draw over decorations such as the status bar.
WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY should be used instead of TYPE_SYSTEM_ALERT in API level >=19.
Bonus notes (if you're reading this, you're probably trying to make a full-screen overlay):
Your manifest will need the following permissions (explanation here):
<uses-permission android:name="android.permission.ACTION_MANAGE_OVERLAY_PERMISSION"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
My understanding is that SYSTEM_ALERT_WINDOW is the actual permission required, but that ACTION_MANAGE_OVERLAY_PERMISSION is needed also: it lets you request at runtime that the user grant the SYSTEM_ALERT_WINDOW privilege.
I think your situation can be solved with an alternative approach.
Create a Full Screen custom view (you can set the appropriate flag in the constructor of the custom view)
Override onSizeChanged() in the custom view. This will give you the Canvas height and width. This is optional. If you want to fill the whole custom view with a translucent color, this won't be necessary.
Use the width and height from above, or simply use canvas.drawColor() in the onDraw() of the custom view.
This will ensure that whenever the view is recreated, it will be redrawn to fill the whole screen(canvas).
I am using these parameters to show an image on the screen. (this is running on a service cause of that i am using windowmanager instead of usual way- the image comes and goes continuesly)
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.TYPE_PHONE,
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
PixelFormat.TRANSLUCENT
);
The Point is when I use FLAG_NOT_TOUCHABLE it allows user's touch go through the Picture, Which is fine and it is what I want. but when there is a need for text entry the keyboard keeps disappering (I want it to be there so I am able to input text even though the Picture is overlapping). this works for buttons and other input but the the keybord vanishes. How can I keep both?
changing WindowManager.LayoutParams.TYPE_PHONE to WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY solved the problem.
I have an android app that displays an system alert from a service:
WindowManager.LayoutParams params = new WindowManager.LayoutParams(WindowManager.LayoutParams.FILL_PARENT, WindowManager.LayoutParams.FILL_PARENT,
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT, WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, PixelFormat.OPAQUE);
((WindowManager) getSystemService(WINDOW_SERVICE)).addView(view, params);
I have set view to a GridView, Scrollable TextView, and a Gallery and on all of them if I scroll the images/text blend together. Everything is blurred together. It is as if the new image/text is drawn on the screen but the old image/text is not removed. Any idea on how to fix this? Does anyone have an example of a TYPE_SYSTEM_ALERT where the content is scrollable?
Below my ListView, I have a bar with a trash can on it.
When some action is performed, suppose it is simply clicking on the item, I would like the item to animate down to the trash can. This means both moving downward and shrinking horizontally to a width of 0.
How can this be achieved? I suspect it involves creating a bitmap of the item and then animating that bitmap downward and ever smaller. This also means, depending on where the list item is relative to the screen, it may need to shrink faster than other times (ie: if the list item is already close to the bottom of the screen instead of the top). I'm just not sure how to achieve any of this.
The only animation I've done involves presenting a bar from the bottom of the screen. I've no experience with moving around a free-floating object and altering its size.
Could someone provide me with some good direction?
Follow these steps to achieve that target:
First, you need to create a "hoovered" copy of your list item. To do that, you need to inflate a new view, which has the same structure as the list items in your listview. After that, you need to add that to the windows manager, like this:
WindowManager.LayoutParams windowParams = new WindowManager.LayoutParams();
windowParams.gravity = Gravity.TOP | Gravity.RIGHT;
windowParams.x = item.getLeft();
windowParams.y = item.getTop();
windowParams.height = item.getHeight();
windowParams.width = item.getWidth();
windowParams.flags =
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
| WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
windowParams.format = PixelFormat.TRANSLUCENT;
windowParams.windowAnimations = 0;
View hooveredView = getClonedView(item);
// Add the hoovered view to the window manager, as a new view in the screen
WindowManager mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
mWindowManager.addView(hooveredView, windowParams);
Now, create new Animation class which gets the WindowParams, and change the x and y coordinates, in order to move the hoovered item to the lower trash bar. Update the new view position by using this code:
mWindowManager.updateViewLayout(mViewToAnimate, mWindowParams);
You can also play with the width and height in order to simulate shrinking of the view.
In order to animate the empty spot left on the list, you can use my ExpandAnimation explained on my blog:
http://udinic.wordpress.com/2011/09/03/expanding-listview-items/
That's pretty much it. If you need more details - just ask.
As far as performing the animation, take a look at Animation. It looks like you will want to build an AnimationSet comprised of a ScaleAnimation, AlphaAnimation, and TranslateAnimation. Then you apply that animation to whatever you create to move around over the list view. Off the top of my head, I would probably approach that aspect by creating a copy of the item that will be deleted, animating it to the 'trash' then deleting the original item. But that approach may be harder/less efficient than creating a Bitmap.