Cordovaの画面を固定サイズで実装する方法


固定の画面サイズで実装されているWebアプリをCordova 10に移植する場合の実装方法です。
※CordovaはAndroidのみです。iOSは使用していません。ですが環境に限らず同じ方法は使えるはずです。

例えば300x400の固定サイズで実装したWebアプリがあるとします。
それをCordova上でスマホの端末の画面いっぱいに拡大表示することで、固定レイアウトのままAndroidアプリを作ってしまう方法です。Webアプリ移植時の変更を最小限にすることができます。
固定幅で作りたいだけならviewportを使う方法もありますが、幅も高さも画面からはみ出ないように柔軟に拡大するためJavaScriptで実装しました。

拡大表示する方法

CSSのtransformを使います。
以下のような記述をすると要素全体を拡大表示できることを利用します。

/* 150%に拡大する場合の例 */
transform:
  translate(25%, 25%)
  scale(1.5, 1.5);

これをJavaScriptで動的に比率を計算して行います。

以下はメインformを画面サイズに合わせて拡大する部分のコードです。(記事の最後に全ソースがあります。)
※Webアプリのサイズが300x400(余白を入れると320x400)の場合のコードです。
※formでなくてもdiv等でも同じやり方で拡大できます。


    // 画面サイズに合わせて拡大
    var windowWidth = (window.innerWidth || document.documentElement.clientWidth || 0);
    var windowHeight = (window.innerHeight || document.documentElement.clientHeight || 0);

    let percent = Math.floor(100 * windowWidth / 320);
    if (windowWidth > windowHeight) {
        percent = Math.min(percent, Math.floor(100 * windowHeight / 400));
    }
    let scale = percent / 100;
    let translate = "" + ((percent - 100) / 2) + "%";

    if (scale > 1) {
        mScale = scale; // グローバル変数に拡大率を保存しておく ※後で説明
        document.forms[0].style.transform =
                "translate(" + translate + ", " + translate + ") " +
                "scale(" + scale + ", " + scale + ")";
    }

固定サイズ座標取得方法

タップした位置の座標は固定サイズの仮想的な座標とはことなるので、マウスの座標のようにタップ位置を取得したい場合は座標変換が必要になります。
以下の部分が拡大表示したときの拡大率を使って座標を変換する部分のコードです。(記事の最後に全ソースがあります。)

    // イベントでの固定サイズ座標取得サンプル
    let getTouchPos = function(toucheInfo) {
        let orgX = toucheInfo.pageX;
        let orgY = toucheInfo.pageY;
        // ※mScale: 拡大表示したときの拡大率
        // ※mAdjustX,mAdjustY: 基準位置(左上)の実際の座標
        let x = (toucheInfo.pageX - mAdjustX) / mScale;
        let y = (toucheInfo.pageY - mAdjustY) / mScale;
        return { orgX, orgY, x, y };
    };
    document.addEventListener("touchmove", function(){
        event.preventDefault(); // デフォルトのイベントを禁止する
        let pos = getTouchPos(event.touches[0]);
        // TODO 何かの処理
        // ※pos.x, pos.yで固定サイズの座標が取得できます。
    }, passiveSupported ? { passive: false } : false);

実際のCordovaのサンプルコード

画面を固定サイズ化(拡大)するサンプルコードを紹介します。
Webアプリのメイン領域を300x300の固定サイズで実装する場合のコードです。
Cordova(10.0)のindex.htmlに上書きすればサンプルの確認ができます。

index.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta http-equiv="Content-Security-Policy" content="default-src *; style-src 'self' 'unsafe-inline'; img-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'"/>
        <meta name="format-detection" content="telephone=no">
        <meta name="msapplication-tap-highlight" content="no">
        <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">
        <meta name="color-scheme" content="light dark">
        <link rel="stylesheet" href="css/index.css">
        <title>固定レイアウト画面サンプル</title>
<script>
window.onerror = function(msg, uri, line) {
    alert(msg + uri + line);
};

var mAdjustX = 0;
var mAdjustY = 0;
var mScale = 1;

// Passive eventサポート有無のチェック
var passiveSupported = false;
try {
  window.addEventListener("test", null, Object.defineProperty({}, "passive", { get: function() { passiveSupported = true; } }));
} catch(err) {}

// ------------------------------------------------------------------------------------------------
// 初期化処理
// ------------------------------------------------------------------------------------------------
function initialize() {

    // フォーム表示によりずれる座標を調整する
    if (document.mainForm.offsetLeft) {
        mAdjustX = document.mainForm.offsetLeft;
        mAdjustY = document.mainForm.offsetTop;
    } else {
        mAdjustX = 0;
        mAdjustY = 0;
    }

    // イベントでの固定サイズ座標取得サンプル
    let getTouchPos = function(toucheInfo) {
        let orgX = toucheInfo.pageX;
        let orgY = toucheInfo.pageY;
        let x = (toucheInfo.pageX - mAdjustX) / mScale;
        let y = (toucheInfo.pageY - mAdjustY) / mScale;
        return { orgX, orgY, x, y };
    };
    let getMousePos = function(event2, event) {
        let orgX, orgY, x, y;
        if (event2.toString() == "[object MouseEvent]") {
            orgX = event2.clientX;
            orgY = event2.clientY;
            x = (event2.clientX - mAdjustX) / mScale;
            y = (event2.clientY - mAdjustY) / mScale;
        } else if(event) {
            orgX = event.clientX;
            orgY = event.clientY;
            x = (event.clientX - mAdjustX) / mScale;
            y = (event.clientY - mAdjustY) / mScale;
        }
        return { orgX, orgY, x, y };
    };
    document.addEventListener("touchstart", function(){
        let pos = getTouchPos(event.touches[0]);
        // TODO 何かの処理
    }, passiveSupported ? { passive: false } : false);
    document.addEventListener("touchend", function(){
        let pos = getTouchPos(event.changedTouches[0]);
        // TODO 何かの処理
    }, passiveSupported ? { passive: false } : false);
    document.addEventListener("touchmove", function(){
        event.preventDefault(); // デフォルトのイベントを禁止する
        let pos = getTouchPos(event.touches[0]);
        // TODO 何かの処理
        el("info").innerText = "本当の座標:(" + pos.orgX + "," + pos.orgY + ") 固定サイズ座標:(" + Math.floor(pos.x) + "," + Math.floor(pos.y) + ")";
    }, passiveSupported ? { passive: false } : false);
    document.addEventListener("mousedown", function(event2){
        let pos = getMousePos(event2, event);
        // TODO 何かの処理
    }, false);
    document.addEventListener("mouseup", function(event2){
        let pos = getMousePos(event2, event);
        // TODO 何かの処理
    }, false);
    document.addEventListener("mousemove", function(event2){
        let pos = getMousePos(event2, event);
        // TODO 何かの処理
        el("info").innerText = "本当の座標:(" + pos.orgX + "," + pos.orgY + ") 固定サイズ座標:(" + Math.floor(pos.x) + "," + Math.floor(pos.y) + ")";
    }, false);

    // 画面サイズに合わせて調整
    var windowWidth = (window.innerWidth || document.documentElement.clientWidth || 0);
    var windowHeight = (window.innerHeight || document.documentElement.clientHeight || 0);

    let percent = Math.floor(100 * windowWidth / 320);
    if (windowWidth > windowHeight) {
        percent = Math.min(percent, Math.floor(100 * windowHeight / 400));
    }
    let scale = percent / 100;
    let translate = "" + ((percent - 100) / 2) + "%";

    if (scale > 1) {
        mScale = scale;
        document.forms[0].style.transform =
                "translate(" + translate + ", " + translate + ") " +
                "scale(" + scale + ", " + scale + ")";
    }
    let gameScreen = el("gameScreen")
    let wallWidth = (windowWidth - gameScreen.getBoundingClientRect().width) / 2;
    el("mainDiv").style.paddingLeft = "" + wallWidth + "px";
    mAdjustX = wallWidth;
    mAdjustY += el("scoreArea").getBoundingClientRect().height;
}

// ------------------------------------------------------------------------------------------------
// HTMLエレメント取得
// ------------------------------------------------------------------------------------------------
function el(strId) {
    return document.getElementById(strId);
}

</script>
<style>
body {
    font-family: sans-serif;
    overflow-y: hidden;
}
* {
    user-select: none;
    -ms-user-select:none;
    -moz-user-select:none;
    -webkit-user-select: none;
    -webkit-touch-callout: none;
}
#mainDiv {
    background-color: #bbbbff;
    text-transform: none;
}
.label {
    display: inline-block;
    text-transform: none;
}
</style>
    </head>
    <body onload="initialize();">
        <!-- メイン -->
        <div id="mainDiv" style="width:100%; height:100%;">
        <form name="mainForm" style="position:relative; width:100%; height:100%;" onsubmit="return false;">
            <!-- 情報表示 -->
            <div>
                <div id="scoreArea" style="height:36px; color:#000080; font-weight:bold;">
                    <div style="font-size:100%; margin-top:2px; line-height:17px;">
                        <div class="label">TODO 情報表示エリア1</div><br>
                        <div class="label">スコアの表示等</div>
                    </div>
                </div>
            </div>
            <!-- スクリーン表示 -->
            <div>
                <div id="gameScreen" style="width:300px; height:300px; position:relative; background-color:#cccccc;">
                    TODO ここは300x300の固定サイズで実装可能
                    <div style="position:absolute; width:100px; height:100px; top: 50px; left: 50px; background:green; ">top: 50px;<br>left: 50px;</div>
                    <div style="position:absolute; width:100px; height:100px; top:100px; left:100px; background:yellow;">top:100px;<br>left:100px;</div>
                    <div style="position:absolute; width:100px; height:100px; top:150px; left:150px; background:red;   ">top:150px;<br>left:150px;</div>
                </div>
            </div>
            <!-- 情報表示2 -->
            <div>
                <div id="scoreArea2" style="height:36px; color:#000080; font-weight:bold;">
                    <div style="font-size:80%; margin-top:2px; opacity:0.5;">
                        <div class="label" style="font-size:90%;">TODO 情報表示エリア2</div><br>
                        <div class="label" id="info">スコア以外の表示等</div>
                    </div>
                </div>
            </div>
        </form>
        </div>

        <script src="cordova.js"></script>
        <script src="js/index.js"></script>
    </body>
</html>

実際にアプリを移植した例

実際に画面固定サイズで実装していたWebアプリ(ブロック崩しゲーム)をCordovaに移植してAndroidアプリを作成しました。
HRGシンプルブロック崩し」としてGoogle Playで公開しています。