ビット反転画像ビューアーアプリ

 2024年4月27日7時頃に、SAF(ストレージ アクセス フレームワーク)の画面から戻るボタンか戻るジェスチャーで戻ってファイルを選択しなかった場合に対応していなくて、WebView内のHTMLのファイル選択ボタンが反応しなく成ってしまう誤りを修正しました。



 Githubでビット反転画像ビューアーアプリの.apkファイルなどをパブリック ドメインで公開しております。

 マイクロソフトのBing検索エンジンで「github eliphas1810-tools」などで検索してみてください。

 残念ながらグーグル検索エンジンでは検索できません。


 オフラインでスマホ内の.htmlファイルをChromeアプリで表示して.htmlファイル内のJavaScriptを起動して処理させる事ができるのですが、次のような操作手順が必要に成ってしまい面倒なので、ある.htmlファイル専用のアプリを自作しました。

 ①GoogleのFilesアプリなどでスマホ内の.htmlファイルを選択する。

 ②.htmlファイルを表示するアプリとしてChromeアプリなどを選択する必要が有ります。

 ③.htmlファイルでファイル選択しようとすると、GoogleのFilesアプリ?などを選択する必要が有ります。


 需要は無いかもしれませんが、他のアプリを自作する時に役立つかもしれないので、書き残しておきます。



 AQUOS sense3で動作を確認できました。



 ※下記のXMLファイルやKotlinのプログラムなどのコードをコピペする場合は、2文字の全角空白を4文字の半角空白に置換してください。


 また、Android StudioにJavaやKotlinなどのプログラムのコードをコピペして、「import android.R」が自動で追加されてしまったら、削除してください。

 「android.R」は、「R.layout.activity_main」や「R.id.◯◯◯」の「R」とは違います。

 そのため、「import android.R」が有ると、コンパイル エラーが発生してしまいます。


 Android StudioにJavaやKotlinなどのプログラムのコードをコピペすると、変数の名前が半角バッククォート記号(`)で囲まれる事が有ります。

 Kotlinでは変数の名前を半角バッククォート記号(`)で囲むと予約語(inやnullなど)や半角空白記号( )などを変数の名前にできるそうです。

 可能であれば、半角バッククォート記号(`)で囲まれた変数の名前は、半角バッククォート記号(`)で囲まずに済む名前に変更したほうが良いのでは、と個人的に思っております。



 ①Android Studioで、アプリに同梱するファイルが置けるassetsディレクトリーを作成します。



 ②assetsディレクトリーにアプリへ同梱する.htmlファイルを置きます。


 ・次の.htmlファイルはオフラインのパソコンのChromeやFirefoxなどで表示して操作して処理させる事ができます。


 ・次の.htmlファイルでは、HTMLのinputタグによってファイルを選択できて、HTMLのbuttonタグによるボタンを押すと、選択したファイルをビット反転された画像ファイルと見なし、ビット反転し直して、画像ファイルのバイナリー データをBase64形式のテキスト データに変換して、Base64形式のテキスト データの画像を表示するHTMLのimgタグを生成します。


 ※ビット反転し直すJavaScriptのコードを削除する修正をすれば、普通の画像ファイルを表示できます。


/home/◯◯◯/AndroidStudioProjects/BitFlippedImageViewer/app/src/main/assets/BitFlippedImageViewer.html

――――――――――――――――――――

<!DOCTYPE html>

<html lang="ja">

  <head>

    <meta charset="UTF-8" />

    <title>ビット反転画像ビューアー</title>

    <style>

img {

  width: 100%;

}

    </style>

  </head>

  <body>

    <div>

      ビット反転画像ファイル選択 <input type="file" id="file" multiple />

    </div>

    <br />

    <div>

      <button type="button" id="showBitFlippedImage">ビット反転画像表示</button>

    </div>

    <br />

    <p id="message"></p>

    <br />

    <div id="images"></div>



    <script>



function $(id) {

  return document.getElementById(id);

}



function readAsArrayBufferSync(file) {

  return new Promise(function (resolve, reject) {

    var fileReader = new FileReader();

    fileReader.onload = function () { resolve(fileReader.result); };

    fileReader.onerror = function () { reject(fileReader.error); };

    fileReader.readAsArrayBuffer(file);

  });

}



$("showBitFlippedImage").onclick = async function () {


  var files = $("file").files;


  if (files.length == 0) {

    $("message").innerHTML = "ビット反転画像ファイルを選択してください。";

    return;

  }


  $("message").innerHTML = "ビット反転画像ファイルを処理中です……。";


  $("images").innerHTML = "";


  for (var index = 0; index < files.length; index++) {


    var file = files[index];


    var arrayBuffer = await readAsArrayBufferSync(file);

    var size = arrayBuffer.byteLength;

    var dataView = new DataView(arrayBuffer);


    //ビット反転してからJavaScriptの「バイナリー文字列」に変換

    var jsBinaryString = "";

    for (var byteIndex = 0; byteIndex < size; byteIndex++) {

      jsBinaryString += String.fromCharCode(((~ dataView.getUint8(byteIndex)) >>> 0) & 0xff)

    }


    var divElement = document.createElement("div");


    var imageElement = document.createElement("img");


    var fileName = file.name;

    if (fileName.match(/[^a-z]png[^a-z]/gi) != null) {

      imageElement.src = "data:image/png;base64," + btoa(jsBinaryString);

    } else {

      imageElement.src = "data:image/jpeg;base64," + btoa(jsBinaryString);

    }


    divElement.appendChild(imageElement);


    $("images").appendChild(divElement);

  }


  $("message").innerHTML = "";

};



    </script>

  </body>

</html>

――――――――――――――――――――

 ◯◯◯はLinux Mintのユーザー名です。

 BitFlippedImageViewerは著者が付けたAndroid Studioのプロジェクトの名前です。



 ③アンドロイドのアプリの「ビュー」と呼ばれる画面の部品は、そのままでは指のジェスチャーのピンチ アウトやピンチ インで拡大縮小できないので、既存の「ビュー」を継承して自作する必要が有ります。


 ・Android Studioでは、「ビュー」を継承して自作すると、何もしなくても、画面の設定の.xmlファイルで利用できるように成ります。


 ・.htmlファイルを表示して操作したいので、拡大縮小できるWebViewを自作します。


 ・ScaleGestureDetector.scaleFactorで拡大縮小率を取得できますが、そのままでは過敏に反応して処理する羽目に成ってしまうので、直近の拡大縮小率を記憶して、0.05単位で拡大縮小率が変化した場合だけ、拡大縮小します。


 ・WebView.zoomIn()で拡大、WebView.zoomOut()で縮小できますが、本来より小さく縮小できない?ようですし、拡大には限界が有るようです。


/home/◯◯◯/AndroidStudioProjects/BitFlippedImageViewer/app/src/main/java/eliphas1810/bitflippedimageviewer/ZoomableWebView.kt

――――――――――――――――――――

package eliphas1810.bitflippedimageviewer


import android.content.Context

import android.util.AttributeSet

import android.view.MotionEvent

import android.view.ScaleGestureDetector

import android.webkit.WebView



class ZoomableWebView(

  context: Context,

  attributeSet: AttributeSet?,

  defaultStyleAttribute: Int,

  defaultStyleResourceId: Int

) : WebView(context, attributeSet, defaultStyleAttribute, defaultStyleResourceId), ScaleGestureDetector.OnScaleGestureListener {



  private val scaleGestureDetector = ScaleGestureDetector(context, this)



  private var lastScaleFactor = 1.0f



  constructor(context: Context, attributeSet: AttributeSet?, defaultStyleAttribute: Int) : this(context, attributeSet, defaultStyleAttribute, 0)

  constructor(context: Context, attributeSet: AttributeSet?) : this(context, attributeSet, 0, 0)

  constructor(context: Context) : this(context, null, 0, 0)



  override fun onTouchEvent(motionEvent: MotionEvent?): Boolean {


    scaleGestureDetector.onTouchEvent(motionEvent!!)


    return super.onTouchEvent(motionEvent)

  }



  override fun onScaleBegin(scaleGestureDetector: ScaleGestureDetector): Boolean {

    return true

  }


  override fun onScale(scaleGestureDetector: ScaleGestureDetector): Boolean {


    if ((lastScaleFactor / 0.05f).toInt() == (scaleGestureDetector.scaleFactor / 0.05f).toInt()) {

      return true

    }


    lastScaleFactor = scaleGestureDetector.scaleFactor


    //ピンチアウトの場合

    //

    //拡大の場合

    //

    if (1.0f < scaleGestureDetector.scaleFactor) {


      zoomIn()


    //ピンチインの場合

    //

    //縮小の場合

    //

    } else {


      zoomOut()

    }


    return true

  }


  override fun onScaleEnd(scaleGestureDetector: ScaleGestureDetector) {

  }

}

――――――――――――――――――――

 ◯◯◯はLinux Mintのユーザー名です。

 BitFlippedImageViewerは著者が付けたAndroid Studioのプロジェクトの名前です。

 eliphas1810/bitflippedimageviewerは著者が付けたJavaやKotlinのプログラムのパッケージのディレクトリの相対パスです。

 eliphas1810.bitflippedimageviewerは著者が付けたJavaやKotlinのプログラムのパッケージの名前です。



 ④WebViewが有る画面の設定の.xmlファイルを作成して、「<WebView」を「<eliphas1810.bitflippedimageviewer.ZoomableWebView」に変更します。


/home/◯◯◯/AndroidStudioProjects/BitFlippedImageViewer/app/src/main/res/layout/activity_main.xml

――――――――――――――――――――

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

  android:orientation="vertical"

  android:layout_width="match_parent"

  android:layout_height="match_parent"

>


  <eliphas1810.bitflippedimageviewer.ZoomableWebView

    android:id="@+id/webView"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

  />


</LinearLayout>

――――――――――――――――――――

 ◯◯◯はLinux Mintのユーザー名です。

 BitFlippedImageViewerは著者が付けたAndroid Studioのプロジェクトの名前です。



 ⑤画面に1対1対応しているアクティビティを用意します。


 ・assetsディレクトリーに置いた.htmlファイルをwebView.loadUrl()は「file:///android_asset/◯◯◯.html」という形式のURIで読み込む事ができるそうです。


 ・Kotlin側で、HTMLのinputタグによるファイル選択のイベントを受け取って、アンドロイドのSAF(ストレージ アクセス フレームワーク)などでファイル選択をさせて、ファイル選択結果をHTMLのJavaScriptへ戻す必要が有るようです。


/home/◯◯◯/AndroidStudioProjects/BitFlippedImageViewer/app/src/main/java/eliphas1810/bitflippedimageviewer/MainActivity.kt

――――――――――――――――――――

package eliphas1810.bitflippedimageviewer


import android.content.Intent

import android.net.Uri

import androidx.appcompat.app.AppCompatActivity

import android.os.Bundle

import android.webkit.ValueCallback

import android.webkit.WebChromeClient

import android.webkit.WebView

import android.widget.*

import androidx.activity.result.ActivityResult

import androidx.activity.result.ActivityResultLauncher

import androidx.activity.result.contract.ActivityResultContracts



class MainActivity : AppCompatActivity() {



  private var valueCallback: ValueCallback<Array<Uri>>? = null



  private var readActivityResultLauncher: ActivityResultLauncher<Intent>? = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult: ActivityResult ->


    if (activityResult.resultCode == RESULT_OK) {

      val intent = activityResult.data


      var uriList = mutableListOf<Uri>()


      //2つ以上のファイルが選択された場合

      if (intent?.clipData?.itemCount != null) {


        for (index in 0..(intent?.clipData?.itemCount!! - 1)) {

          val uri = intent?.clipData?.getItemAt(index)?.uri as Uri


          uriList.add(uri!!)

        }


        //1つ以下のファイルが選択された場合

      } else {


        if (intent?.data != null) {


          val uri = intent?.data as Uri


          uriList.add(uri!!)

        }

      }


      valueCallback?.onReceiveValue(uriList.toTypedArray())

    }


    if (activityResult.resultCode == RESULT_CANCELED) {

      var uriList = mutableListOf<Uri>()

      valueCallback?.onReceiveValue(uriList.toTypedArray())

    }

  }



  override fun onCreate(savedInstanceState: Bundle?) {

    try {

      super.onCreate(savedInstanceState)

      setContentView(R.layout.activity_main)



      val webView = findViewById<ZoomableWebView>(R.id.webView)

      webView?.settings?.javaScriptEnabled = true //JavaScriptを有効化。デフォルトは無効。


      webView?.settings?.allowFileAccess = true //ファイル アクセスを有効化。デフォルトは無効。


      webView?.webChromeClient = object : WebChromeClient() {


        override fun onShowFileChooser(

          webView: WebView?,

          filePathCallback: ValueCallback<Array<Uri>>?,

          fileChooserParams: FileChooserParams?

        ): Boolean {


          valueCallback = filePathCallback


          val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)

          intent.addCategory(Intent.CATEGORY_OPENABLE)

          intent.type = "*/*"

          intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)

          readActivityResultLauncher?.launch(intent)


          return true

        }

      }

      webView?.loadUrl("file:///android_asset/BitFlippedImageViewer.html")



    } catch (exception: Exception) {

      Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

      throw exception

    }

  }



  override fun onDestroy() {

    try {



      valueCallback = null



      readActivityResultLauncher?.unregister()

      readActivityResultLauncher = null



    } catch (exception: Exception) {

      Toast.makeText(applicationContext, exception.toString(), Toast.LENGTH_LONG).show()

      throw exception

    } finally {


      super.onDestroy()

    }

  }

}

――――――――――――――――――――

 ◯◯◯はLinux Mintのユーザー名です。

 BitFlippedImageViewerは著者が付けたAndroid Studioのプロジェクトの名前です。

 eliphas1810/bitflippedimageviewerは著者が付けたJavaやKotlinのプログラムのパッケージのディレクトリの相対パスです。

 eliphas1810.bitflippedimageviewerは著者が付けたJavaやKotlinのプログラムのパッケージの名前です。

  • Xで共有
  • Facebookで共有
  • はてなブックマークでブックマーク

作者を応援しよう!

ハートをクリックで、簡単に応援の気持ちを伝えられます。(ログインが必要です)

応援したユーザー

応援すると応援コメントも書けます

新規登録で充実の読書を

マイページ
読書の状況から作品を自動で分類して簡単に管理できる
小説の未読話数がひと目でわかり前回の続きから読める
フォローしたユーザーの活動を追える
通知
小説の更新や作者の新作の情報を受け取れる
閲覧履歴
以前読んだ小説が一覧で見つけやすい
新規ユーザー登録無料

アカウントをお持ちの方はログイン

カクヨムで可能な読書体験をくわしく知る