3

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.

initial render is fine

The problem emerges when you rotate the device.

view does not rotate with 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.

Birchlabs
  • 7,437
  • 5
  • 35
  • 54
  • I think this is a duplicate issue to this one http://stackoverflow.com/a/10107525/5870896 – Rashid Jan 04 '17 at 02:13
  • How about having 2 views. One for portrait and one for landscape with correct width and height for each. When orientation change occur, just remove the irrelevant view and add the valid view to windowmanager. – Kartik Sharma Jan 04 '17 at 02:37
  • @SecretCoder I believe my code already resembles the answer to that question, since that is one of the references I used to build my code. The only difference is that the linked answer grabs a reference to the View by using the inflator, whereas my code constructs an instance of a View inline in a Service. Given that I have comparable code to that answer and _still_ experience the problem: I would say that the linked issue does _not_ answer my question and is therefore _not_ the same issue. – Birchlabs Jan 04 '17 at 03:10
  • @KartikSharma that could work. is it overkill? ideally I'd want to avoid adding too much overhead to the user's screen rotation. I suspect also that there would be a gap during which the overlay disappears; I'd prefer to avoid things that look glitchy. I did have another thought: maybe I could just make the canvas be square — with both dimensions set to `Math.max(metrics.widthPixels, metrics.heightPixels)`. that way, the same canvas could be used for either orientation (just need to be cognizant of where the screen boundaries are). – Birchlabs Jan 04 '17 at 03:17
  • can you please add full code? – Kishore Jethava Jan 04 '17 at 04:57
  • @kishorejethava the question already contains a link to the full source code. I'll repeat it here for convenience: https://github.com/Birch-san/touhou-walk – Birchlabs Jan 04 '17 at 14:15

2 Answers2

2

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:

enter image description here

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.

Community
  • 1
  • 1
Birchlabs
  • 7,437
  • 5
  • 35
  • 54
0

I think your situation can be solved with an alternative approach.

  1. Create a Full Screen custom view (you can set the appropriate flag in the constructor of the custom view)
  2. 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.
  3. 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).

Henry
  • 17,490
  • 7
  • 63
  • 98
  • My objective is **not** to draw a color over my entire canvas boundaries — I was drawing that box only for illustrative purposes (to demonstrate that my view ignores rotation, and that its dimensions don't update compared to their initial value). But step 2 sounds like exactly what I want, so I tried it. Unfortunately, my view's `onSizeChanged()` is _only_ triggered once (on startup; it changes from 0x0 to 1080x1920). Rotating the device does **not** trigger this event. (And yes, my service is subscribed to `android:configChanges="keyboardHidden|orientation|screenSize"` in my manifest). – Birchlabs Jan 04 '17 at 22:50
  • An amendment: `onSizeChanged()` _does_ get triggered upon orientation changes, but _only_ if your layout has dynamic dimensions. The `WindowManager.LayoutParams()` construction that I was using originally, had _explicitly_-set dimensions —`WindowManager.LayoutParams.FLAG_FULLSCREEN` was interpreted as an integer value 1024 for width and height — and as a result the view's size never changed. But `onSizeChanged()` _does_ get triggered if used in conjuction with the `WindowManager.LayoutParams()` construction in [my answer](http://stackoverflow.com/a/41474994/5257399). – Birchlabs Jan 05 '17 at 17:19