Androidアプリ開発

他のアプリ画面の上に重ねて表示する
WindowManager

この記事は約35分で読めます。
記事内に広告が含まれています。
スポンサーリンク

この記事は Androidスマホ用のアプリ開発の中で、
今後の開発で再使用性が高いと思われるコーディングをまとめたものです。
Java での開発経験、XML構文規則、Android のアプリ開発経験がある方を対象としています。
Android のアプリ開発でお役にたててれば、嬉しいです。
(これから Android のアプリ開発や Java での開発を始めたい方への案内は、
記事の最後で紹介します)

この記事のテーマ


他のアプリ画面の上に重ねてフローティングアイコンを表示する

いち早く最新のAndroidを使うなら、このスマホがオススメです。

ポイント

Andoidではアプリを起動すると、画面に表示しているアプリはバックグラウンドに移動します。
このため同じ画面上に2つのアプリを表示することができません(マルチウィンドウで別ウィンドウに表示は可能)
アプリの画面のスナップショットを取得したいなど、操作パネルを他のアプリの上に重ねて表示したいケースがあります。
今回は他のアプリの上に重ねて操作パネル(フローティングアイコン)を表示する実装を紹介します。

SYSTEM_ALERT_WINDOW

SYSTEM_ALERT_WINDOW

他のアプリの上にもViewを表示するためには、SYSTEM_ALERT_WINDOW権限とユーザ承認が必要です。
使用する権限はマニフェストに記述します。
ユーザ承認はアプリの画面のスナップショットを取得するためのMediaProjectionのユーザ承認の流れで行います。

システム アラート ウィンドウの変更

AndroidManifest.xml

<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    :
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
    :
    <application
        :
        <service
            android:name=".service.SnapService"
            android:foregroundServiceType="mediaProjection" />
        :
    </application>
</manifest>

他のアプリの上にViewを表示する場合、Serviceで実装する必要があります。
マニフェストに追加する権限は、Serviceの実装に必要なPOST_NOTIFICATIONS権限、他のアプリの上にViewを表示するためのSYSTEM_ALERT_WINDOW権限、アプリの画面のスナップショットを取得するためのFOREGROUND_SERVICE_MEDIA_PROJECTION権限を追加します。
ServiceforegroundServiceTypemediaProjectionを設定します。

Activity : ユーザ承認

Android13以上ではServiceを起動する場合にPOST_NOTIFICATIONS権限が必要です。
メニュー(ActionMenuView)のキャプチャを取得する(アプリの画面のスナップショットを取得する)のタップで、POST_NOTIFICATIONSのユーザ承認を確認します。
ユーザ承認を取得(Android12以前は確認不要)している場合、MediaProjectionManagerIntentを取得します。
取得したIntentにクラス(SnapService.class)をセットして、Serviceを起動します。

public class MainActivity extends AppCompatActivity {
    private static final int        REQUEST_MULTI_PERMISSIONS = 301;
    private MediaProjectionManager  mediaProjectionManager;
    private ActionMenuView          actionMenuView;
    private BroadcastReceiver       broadcastReceiver;
    private LocalBroadcastManager   localBroadcastManager;
    :
    // ActivityResultLauncher //
    private final ActivityResultLauncher<Intent> activityResultLauncher= registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
            result -> {
                if (result.getResultCode() == Activity.RESULT_OK) {
                    :
                    switch (menu) {
                        :
                        case 8: // キャプチャを取得する
                            if (result.getData() != null) {
                                capture(result.getData());
                            }
                            break;
                            :
                    }
                } else if (menu == 8) {
                    mediaProjectionManager = null;
                }
            });
    :
    // キャプチャを取得する
    private void capture(Intent intent) {
        intent.setClass(this, SnapService.class);
        intent.setAction(SnapService.START);
        startForegroundService(intent);
        moveTaskToBack(true);
    }
    :
    private void checkPermissions() {
        ArrayList<String> requestPermissions = new ArrayList<>();
        // 通知(33以上)
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S_V2) {
            if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
                requestPermissions.add(Manifest.permission.POST_NOTIFICATIONS);
            }
        }
        if (!requestPermissions.isEmpty()) {
            ActivityCompat.requestPermissions(this, requestPermissions.toArray(new String[0]), REQUEST_MULTI_PERMISSIONS);
        } else {
            mediaProjectionManager = (MediaProjectionManager) getSystemService(Service.MEDIA_PROJECTION_SERVICE);
            activityResultLauncher.launch(mediaProjectionManager.createScreenCaptureIntent());
        }
    }
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (DEBUG) Log.d(TAG, "onRequestPermissionsResult");
        if (requestCode == REQUEST_MULTI_PERMISSIONS) {
            if (grantResults.length > 0) {
                for (int i = 0; i < permissions.length; i++) {
                    if (permissions[i].equals(Manifest.permission.POST_NOTIFICATIONS)) {
                        if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
                            mediaProjectionManager = (MediaProjectionManager) getSystemService(Service.MEDIA_PROJECTION_SERVICE);
                            activityResultLauncher.launch(mediaProjectionManager.createScreenCaptureIntent());
                        }
                    }
                }
            }
        }
    }

    protected void onCreate(Bundle savedInstanceState) {
        :
        // メニュー
        actionMenuView = findViewById(R.id.menu);
        actionMenuView.setVisibility(View.VISIBLE);
        actionMenuView.getMenu().removeGroup(Menu.NONE);
        :
        actionMenuView.getMenu().add(Menu.NONE, 8, Menu.NONE, context.getString(R.string.menu_snap));
        actionMenuView.setOnMenuItemClickListener(menuItem -> {
                menu = menuItem.getItemId();
                Intent intent;
                switch (menu) {
                    :
                    case 8: // キャプチャを取得する
                        if (!Settings.canDrawOverlays(this)) {
                            intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName"));
                            activityResultLauncher.launch(intent);
                        }
                        checkPermissions();
                        break;
                        :
                }
            }
            return false;
        });
        :
        // サービス通信用レシーバ設定
        if (broadcastReceiver == null) {
            broadcastReceiver = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    if (intent.getLongExtra("ALIVE", 0) > 0) {
                        actionMenuView.setVisibility(View.INVISIBLE);
                    } else {
                        actionMenuView.setVisibility(View.VISIBLE);
                    }
                }
            };
            localBroadcastManager = LocalBroadcastManager.getInstance(getApplicationContext());
            final IntentFilter filter = new IntentFilter();
            filter.addAction("SEND_MESSAGE");
            localBroadcastManager.registerReceiver(broadcastReceiver, filter);
        }
    }
    :
    protected void onDestroy() {
        localBroadcastManager.unregisterReceiver(broadcastReceiver);
        Intent intent = new Intent(this, SnapService.class);
        intent.setAction(SnapService.STOP);
        stopService(intent);
        super.onDestroy();
    }
    :
}

Serviceの起動後、ActivitymoveTaskToBackでバックグラウンドで待機します。
また、Service実行中はBroadcastReceiverServiceからのALIVE通信を監視、多重起動ができないようにします。
Activityの終了(onDestroy )で同時にServiceも終了します。

ACTION_MANAGE_OVERLAY_PERMISSIONのユーザ承認画面(Android11)

Service : フローティングアイコンの表示

Activityで取得したMediaProjectionManagerIntentからMediaProjectionを取得します。
MediaProjectionが取得できたら、フローティングアイコンを表示します。
フローティングアイコンはWindowManagerで取得したView上にImageViewとして配置します。
setImageResourceでアイコン画像、setBackgroundResourceで背景、LayoutParamsで表示位置を設定します。
フローティングアイコンの移動やタップはTouchListenerを使用します。

public class SnapService extends Service {
    private static final String     TAG = SnapService.class.getSimpleName();
    private static final int        REQUEST_CODE = 3;
    private static final int        ID = 301;
    private Context                 context;
    private MediaProjection         mediaProjection;
    private MediaProjection.Callback
                                    callback;
    private LocalBroadcastManager   localBroadcastManager;
    private Handler                 handler = new Handler(Looper.getMainLooper());
    private Runnable                runnable;
    public static final String      START = "START";
    public static final String      STOP = "STOP";
    // インタフェース
    private interface OnClickListener {
        void onClick();
    }
    private interface OnCaptureListener {
        void onCapture(Bitmap bitmap);
    }
    // クラス
    private static class Position {
        float    fx, fy;
        int      ix, iy;
        Position(float x, float y) {
            fx = x;
            fy = y;
            ix = (int) x;
            iy = (int) y;
        }
    }
    private class FloatingButton {
        android.view.WindowManager
                    windowManager;
        WindowManager.LayoutParams
                    layoutParams;
        ImageView   imageView;
        Position    initial = new Position(0, 0);
        @SuppressLint("ClickableViewAccessibility")
        private FloatingButton(Context context, OnClickListener onClickListener) {
            DisplayMetrics displayMetrics =  context.getResources().getDisplayMetrics();
            windowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
            imageView = new ImageView(context);
            imageView.setImageResource(R.drawable.service);
            imageView.setBackgroundResource(R.drawable.bg_circle);
            layoutParams = new WindowManager.LayoutParams(
                    android.view.WindowManager.LayoutParams.WRAP_CONTENT,
                    android.view.WindowManager.LayoutParams.WRAP_CONTENT,
                    android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
                    android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                    PixelFormat.TRANSLUCENT);
            layoutParams.gravity = Gravity.TOP | Gravity.START;
            layoutParams.x = (int) ((displayMetrics.widthPixels - (40 * displayMetrics.density)) * 0.5);
            layoutParams.y = (int) (displayMetrics.heightPixels);
            imageView.setOnTouchListener(new View.OnTouchListener() {
                final int  MAX_CLICK_DURATION = 300;
                long time = 0;
                @SuppressLint("ClickableViewAccessibility")
                @Override
                public boolean onTouch(View view, android.view.MotionEvent motionEvent) {
                    switch (motionEvent.getAction()) {
                        case ACTION_DOWN:
                            initial.fx = layoutParams.x - motionEvent.getRawX();
                            initial.fy = layoutParams.y - motionEvent.getRawY();
                            time = System.currentTimeMillis();
                            break;
                        case ACTION_MOVE:
                            if (initial != null) {
                                layoutParams.x = (int) (initial.fx + motionEvent.getRawX());
                                layoutParams.y = (int) (initial.fy + motionEvent.getRawY());
                                windowManager.updateViewLayout(view, layoutParams);
                            }
                            break;
                        case ACTION_UP:
                            time = System.currentTimeMillis() - time;
                            if (time < MAX_CLICK_DURATION) {
                                time = System.currentTimeMillis();
                                ExecutorService executorService = Executors.newSingleThreadExecutor();
                                executorService.execute(() -> {
                                    windowManager.removeView(imageView);
                                    imageView = null;
                                    new Handler(Looper.getMainLooper()).post(onClickListener::onClick);
                                });
                            }
                    }
                    return true;
                }
            });
        }
        private void visible(Boolean visible) {
            if (imageView != null) {
                if (visible) {
                    windowManager.addView(imageView, layoutParams);
                } else {
                    windowManager.removeView(imageView);
                }
            }
        }
    }
    :
    private FloatingButton  floatingButton  = null;
    :

    @Override
    public void onCreate() {
        super.onCreate();
        context = getApplicationContext();
        // サービス通信用レシーバー
        localBroadcastManager = LocalBroadcastManager.getInstance(context);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        String action = intent.getAction();
        if (action == null || action.equals(START)) {
            PendingIntent pendingIntent1 = PendingIntent.getActivity(context, REQUEST_CODE, intent, PendingIntent.FLAG_IMMUTABLE);
            NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
            NotificationChannel channel = new NotificationChannel(TAG, TAG, NotificationManager.IMPORTANCE_DEFAULT);
            channel.setSound(null, null);
            channel.enableLights(false);
            channel.setLightColor(R.color.blue);
            channel.enableVibration(false);
            if (notificationManager != null) {
                notificationManager.createNotificationChannel(channel);
                Notification notification = new NotificationCompat.Builder(context, TAG)
                        .setContentTitle(context.getString(R.string.app_name))
                        .setContentText(TAG)
                        .setSmallIcon(R.drawable.ic_camera)
                        .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.service))
                        .setContentIntent(pendingIntent1)
                        .addAction(new NotificationCompat.Action.Builder(R.drawable.ic_round_cancel, context.getString(R.string.menu_exit),
                                PendingIntent.getBroadcast(getApplicationContext(), 0 , new Intent(context, NotificationReceiver.class).setAction(DELETE_NOTIFICATION), PendingIntent.FLAG_IMMUTABLE)).build())
                        .setDeleteIntent(PendingIntent.getBroadcast(getApplicationContext(), 0 , new Intent(context, NotificationReceiver.class).setAction(DELETE_NOTIFICATION), PendingIntent.FLAG_IMMUTABLE))
                        .build();
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    startForeground(ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
                } else {
                    startForeground(ID, notification);
                }
                MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getSystemService(Service.MEDIA_PROJECTION_SERVICE);
                mediaProjection = mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, intent);
                callback = new MediaProjection.Callback() {
                    @Override
                    public void onCapturedContentResize(int width, int height) {
                    }
                    @Override
                    public void onCapturedContentVisibilityChanged(boolean isVisible) {
                    }
                    @Override
                    public void onStop() {
                        super.onStop();
                    }
                };
                mediaProjection.registerCallback(callback, null);
                startOverlay();
            }
        } else {
            stopForeground(true);
            stopOverlay();
        }
        return START_NOT_STICKY;
    }
    private void startOverlay() {
        floatingButton = new FloatingButton(this, () -> {
            if (mediaProjection != null && capture != null) {
                :
            }
        });
        :
        floatingButton.visible(true);
        // ======================================================================
        // ALIVE送信
        // ======================================================================
        handler = new Handler(Looper.getMainLooper());
        runnable = new Runnable() {
            @Override
            public void run() {
                Intent intent = new Intent().setAction("SEND_MESSAGE");
                intent.putExtra("ALIVE", System.currentTimeMillis());
                localBroadcastManager.sendBroadcast(intent);
                handler.postDelayed(this, 1000);
            }
        };
        handler.post(runnable);
    }
    private void stopOverlay() {
        if (floatingButton != null) {
            floatingButton.visible(false);
            floatingButton = null;
        }
        :
        Intent intent = new Intent().setAction("SEND_MESSAGE");
        intent.putExtra("ALIVE", 0);
        localBroadcastManager.sendBroadcast(intent);
    }

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

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (handler != null) handler.removeCallbacks(runnable);
        stopForeground(true);
        stopOverlay();
        if (mediaProjection != null) {
            mediaProjection.unregisterCallback(callback);
            mediaProjection = null;
        }
        ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.AppTask> appTasks = activityManager.getAppTasks();
        if (!appTasks.isEmpty()) {
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
                activityManager.moveTaskToFront(appTasks.get(0).getTaskInfo().taskId, 0);
            } else {
                activityManager.moveTaskToFront(appTasks.get(0).getTaskInfo().id, 0);
            }
        }
    }
}

フローティングアイコンのダブルタップ、IntentActionがSTARTでない場合にServiceを終了します。
Service終了時はフローティングアイコンを非表示、MediaProjectionを開放します。
その後、バックグラウンドで待機しているActivitymoveTaskToFrontでフォアグラウンドに復帰させます。
Service起動中はLocalBroadcastManagerでALIVE通信を一定間隔で送信します。

アプリの上にフローティングアイコン(imgsaw)を表示

今回は、ここまでです。

他のアプリの上に重ねてフローティングアイコンを表示しているAndroidアプリです。

誤字脱字、意味不明でわかりづらい、
もっと詳しく知りたいなどのご意見は、
このページの最後にある
コメントか、
こちらから、お願いいたします♪

ポチッとして頂けると、
次のコンテンツを作成する励みになります♪

ブログランキング・にほんブログ村へ

これからAndroidのアプリ開発やJavaでの開発を始めたい方へ

アプリケーション開発経験がない方や、アプリケーション開発経験がある方でも、Java や C# などのオブジェクト指向言語が初めての方は、Android のアプリ開発ができるようになるには、かなりの時間がかかります。
オンラインスクールでの習得を、強くおススメします。

未経験者からシステムエンジニアを目指すのに最適です。まずは無料相談から♪

未経験者からプログラマーを目指すのに最適です。まずは無料カウンセリングから♪

カリキュラムとサポートがしっかりしています。お得なキャンペーンとかいろいろやっています♪

ゲーム系に強いスクール、UnityやUnrealEngineを習得するのに最適です。まずは無料オンライン相談から♪

参考になったら、💛をポッチとしてね♪

スポンサーリンク
msakiをフォローする
スポンサーリンク

コメント欄

タイトルとURLをコピーしました