goo blog サービス終了のお知らせ 

タブレット用プログラムの書き止め

android OS & iPadOS の記録。

【kotlin】 Floating Menu 自作

2024-03-18 15:56:55 | Android studio 日記

ディレクトリの移動の前にコンテキストメニューのようなものを作る。

画像をロングプレスするとお気に入りリストにパスを登録してたんだけど、
その場所にメニューを開いて機能選択できたら色々都合が良いかなぁ。

別のフラグメントを用意するのではなく、今のフラグメントにViewをXMLに追加してON、OFFする。
台紙部分をGONEすれば、無いものとなるので楽だ。
非表示で表示場所(X,Y)を設定して表示させる。移動はさせないものとする。

問題もある。
Viewをはじめに描画構築した時は表示直後にそのViewのプロパティを参照しても正常値が戻ってこない。更新タイムラグ。

メニューの表示場所をスクリーン内に修正表示させる場合はフラグメント切り替え時のView表示初回のみ位置修正ができない。
Viewの幅と高さが0のままで本当の数値が取得できないのでメニューが右外、下外に出てしまったらスクリーン内に戻せない。

ただ、XMLでメニューの大体の大きさは推し量ることができる。
それをDimens.xml でMenuWidth、MenuHeightと登録しておく。


【fragment_main.xml】

    < LinearLayout
        android:id="@+id/menu"
        android:padding="20dp"
        省略
        >

        < TextView
            android:layout_width="@dimen/menu_width"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            省略
            />

        < TextView
            android:layout_width="@dimen/menu_width"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            省略
            />

   

android:padding と android:layout_margin でメニュー数が変わっても大体の高さを推測できる。


【Dimens.xml】
    < dimen name="menu_width">200dp</dimen>
    < dimen name="menu_height">130dp</dimen>
    < dimen name="menu_offset">40dp</dimen>


【MainFragment.kt】

class MainFragment : Fragment() {
    // 省略

    private fun initFloatingMenu() {
        val menu: LinearLayout = requireActivity().findViewById(R.id.menu)
        menu.visibility = View.GONE

        val menuTextView1: TextView = requireActivity().findViewById(R.id.menu1)
        menuTextView1.setOnClickListener(View.OnClickListener {
            //
        })

        val menuTextViewDone: TextView = requireActivity().findViewById(R.id.menuDone)
        menuTextViewDone.setOnClickListener(View.OnClickListener {
            off_FloatingMenu()
        })
    }
    private fun off_FloatingMenu() {
        val menu: LinearLayout = requireActivity().findViewById(R.id.menu)
        menu.visibility = View.GONE

        waitFlag = false
    }


    // ロングプレスのタップ位置が渡されてくる

    private fun on_FloatingMenu(px:Float, py:Float) {
        val menu: LinearLayout = requireActivity().findViewById(R.id.menu)

        val w:Float = requireContext().resources.getDimension(R.dimen.menu_width) +
                requireContext().resources.getDimension(R.dimen.menu_offset) * 2

        var x:Float = px - w * 3/4
        if ( x < 0 )
            x = 20f
        else if ( px >= ( sm!!.getScreenWidth( requireContext() ) - w * 3/4) )
             x = sm!!.getScreenWidth( requireContext() ) - w

        val h:Float = requireContext().resources.getDimension(R.dimen.menu_height)
        var y:Float = py - h + requireContext().resources.getDimension(R.dimen.menu_offset)

        if ( y < 0 )
             y = 20f
        else if ( py >= ( sm!!.getScreenHeight( requireContext() ) -
             requireContext().resources.getDimension(R.dimen.menu_offset) * 2 ) )

            y = sm!!.getScreenHeight( requireContext() ) - h -
             requireContext().resources.getDimension(R.dimen.menu_offset)

        menu.x = x
        menu.y = y

        menu.visibility = View.VISIBLE
        waitFlag = true
    }
}

タップ位置からメニューのオフセット処理をする。-dx、-dy 左上にオフセット。
これは一番下に何もせずに帰るボタンを設置。そのボタンを指の下に表示させるため。

オフセットの結果、メニューがスクリーンの外に出てしまう場合にメニューをスクリーン上に戻し表示させる修正を加える。

2回の処理の後にViewの左上の座標をX,Yに代入。

 

sm!!.getScreenWidth( Context )
Context を渡すと現在のスクリーン幅を返すメソッド。

 


  • X
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

【kotlin】 サムネイル表示で非同期処理を使いたい

2024-03-16 02:37:56 | Android studio 日記

サムネイル表示のフラグメントをJavaからKotlinへ変換中なんだけど、
Javaの時に思い付きでコードを組んでいたのでもう少しまともの形にしたい。

サムネイル画像を非同期で別々に単独で作成処理をしてキャッシュフォルダに保存する。
フラグメントが終了するとき、未完了の非同期処理を一斉にキャンセルさせる。

コルーチンの説明でキャンセルがあったからその辺を攻めてみる。

気が重い...。

 


Javaの時は
スレッドプールにスレッドを積んでいき、指定の制限数のスレッドを実行してきた。
実行場所もUIかバックかをいつも意識してデータのやり取りも注意が必要だった。

Kotlinのコルーチンは、
スコープでUIかバックかを記載できる。同一メソッドの中でOK?
ビューにアクセスするときはUI部分で記載するのは同じ。コルーチンでは"post()"を使用した。
コルーチンのキャンセルもフラグメントの消滅に合わせてフラグメント内のすべてにキャンセルが実行されるみたい。便利。


RecyclerViewで使うサムネイルを作成してキャッシュファイルに保存する。
システムからサムネイルを呼び出せばいいんだけれど、対象が古いAPIなので自分で作ってキャッシュに入れる。

サムネイルFragment 専用になるからインナークラスでも良いのかな?
とりあえず、やってみて対応しよう。

class MyFragment : Fragment() {
    /* 色々省略 * /

    inner class MyAdapter() : RecyclerView.Adapter() {
        /* 色々省略 * /

        private fun loadImage(position: Int): Bitmap? {
            /* 色々省略 * /

            /* 受け取ったポジションから絶対パスネームを取り出し、キャッシュファイルの絶対パスを調べる。 * /
            if (!cf.exists()) {
                /* 色々省略 * /

                /* キャッシュファイルが無い時はコルーチンへ * /
                runBlocking {
                    asyncTask(imageAbsPaths!!.get(position), recyclerView!!, position)
                }

                /* 一旦リソースの画像をセットしておく。 * /
                return BitmapFactory.decodeResource(
                    requireContext().getResources(),
                    R.drawable.img_file
                )
            }
            return BitmapFactory.decodeFile(cf.absolutePath) /* キャッシュファイルがあれば、Bitmapを返す * /
        }
        /* 色々省略 * /
    }

    private val parentJob = Job()  /* 一度に全部にキャンセルを通知する小細工 * /

    suspend fun asyncTask(path:String, rv: RecyclerView, vID: Int) {  /* 引数はアダプター内の値を受け取っている * /

        val saveSize = requireContext().getResources()
                .getDimension(R.dimen.thumbnail_save_size).toInt()  /* 画像の大きさをResに宣言している * /
        val souFile: File = File(path)
        if (!souFile.exists() || !souFile.isFile) {
            return
        }
        val cacheDir: File? = viewModel.getCacheDir_thumbnail()  /* キャッシュディレクトリを取得 * /
        if (cacheDir == null || !cacheDir.exists() && !cacheDir.mkdirs()) {
            return
        }
        val cacheFile = File(cacheDir, souFile.name)  /* キャッシュファイルのパスを作成 * /
        if (cacheFile.exists()) return  /* キャッシュファイルが存在してたら帰る。無いから来たんだけども * /

        runBlocking {  /* コルーチンへ * /
            val childJob = lifecycleScope.async(parentJob) {  /* 返り値を受け取りたいのでasync() * /
                MyMakeThumbnail().toThumbnailFile(souFile, cacheFile, saveSize)  /* 外部発注、キャッシュ保存 * /
            }

            val result: Int = childJob.await()

            when (result) {
                0 -> if (vID < rv.adapter!!.itemCount) {

                   /* 直接Viewにアクセスすると例外が発生するので更新はpost()を使用 * /
                    rv.post(Runnable { 
                        rv.adapter!!.notifyItemChanged(vID)  /* 保存完了後にポジションを更新させる * /
                    })
                }

                /* 色々省略 * /
            }
        }
    }

    override fun onDestroy() {
        parentJob.cancel()     /* 親をキャンセルすると子供全部にキャンセル通知してくれる * /
        super.onDestroy()
    }
}

lifecycleScopeなのでフラグメントが終了すれば、キャンセルになるのだけれど念のため。
ベースはできていたので、コルーチンの部分だけで数時間ほど検索&トライで完成できました。

自分の構造に合う書式説明が無かったので色々と組み合わせた後に例外を取り除く感じでした^^;
意外とあっさり動いた気がする。
内容もスッキリだし、Javaの AsyncTask()って何だったの準備とか手順が多すぎ。

次はディレクトリ選択フラグメントかな。

 


  • X
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

Activity -> FragmentA -> FragmentB -> FragmentA で例外発生

2024-03-13 23:49:20 | Android studio 日記

class MainViewModel(app:Application) : AndroidViewModel(app) {

    private var _screenManager: MyScreenManager? = null
    val screenManager: MyScreenManager?
        get() = _screenManager

    init {
        _screenManager = MyScreenManager(getApplication())
    }

}

class FragmentA : Fragment() {

    private val viewModel: MainViewModel by activityViewModels()
    private var sm: MyScreenManager? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        sm = viewModel.screenManager
    }

    override fun onDestroy() {
        sm.finish()
        sm = null
        super.onDestroy()
    }
}

MainViewModel のプロパティをFragmentAで参照していて、
FragmentBに移る時にFragmentBでは、viewModel.screenManagerは使っていないので
FragmentAの sm で取り扱ってデータをクリアして、FragmentBに移った。

そして、FragmentBからFragmentAに戻ると例外で停止する。なぜ?

MainActivityからFragmentAへ遷移は問題なし。
FragmentBからFragmentAに遷移で例外停止。?


悩みに悩んで試行してもダメ。
プログラムコードを考えるのを止めて、機能でブロック的に考察すると
遷移の違いは、onDestroy() の実行の有無。判明した事は...。

MainViewModel の screenManager の保存しているデータをクリアしていました。
FragmentAが再開するときにそれが無かったので例外停止することに...。


sm.finish() が、viewModel.screenManager内のデータをクリアしてるのを気が付くのに時間かかった...。
sm.finish() を削除したら期待通りに動いた。 疲れたぁ。

 


  • X
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

【kotlin】 ViewModel で Context を取り扱う

2024-03-11 20:10:06 | Android studio 日記

class.method(Context) 引数でContextを渡して済ませていた。
それができない状況が出て、ViewModel で Context を取得できるように変えた。

ViewModel のプロパティにContextを保存すると、更新で不一致になったり、参照が残るとシステムが廃棄ができなくなりメモリリークを発生させる。
なので、AndroidViewModel() を使い、getContext() が使えるようにする。

 

class MainViewModel(app:Application) : AndroidViewModel(app) {

    fun getContext(): Context {
        return getApplication()
    }
}


利用する場所では、今まで通りで対応はなし。違いは吸収されるらしい。

 


  • X
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

【kotlin】 画像ビュワー画像送りを再考

2024-03-09 01:23:46 | Android studio 日記

タブレット横長の状態でJPG、PNGを表示。
ディレクトリ単位でリスト化してインデックスNoで表示画像をコントロール。


表示モードは1枚、2枚単位で。
1枚表示モードで画像が縦長1枚は270度回転。
2枚表示モードで縦長2枚は左右並びで2枚表示。
画像が横長なら1枚表示に切り替え。

 

ファイルフルパスをリストアップして順番に取り出し表示させるのが基本。
1枚か2枚の固定であれば難しくないのだが。

条件分岐させるので面倒である。

Javaの時は複雑になってしまった。
Kotlinでは簡素化を心掛けデータ構成。

現在表示されているファイルリストの
Index Noを TargetNo
ファイル名を TargetFileName
そのタイプを TargetShape  // 縦長、横長
サブファイル TargatSubFileName
サブタイプを TargetSubShape

次の表示の為に準備する情報を
NextTargetNo
NextTargetFileName
NextTargetShape
NextTargatSubFileName
NextTargetSubShape

画像の形状を調べてプロパティに代入。
形状を絡めた2枚表示の進む、戻るの分岐が面倒だ。


TargetNo
TargetFileName
TargetShape
TargatSubFileName
TargetSubShape

TargetNoが基準となる。
1枚表示なら、
NextTargetNo = TargetNo+1 or TargetNo-1

で、その結果を

NextTargetNo
NextTargetFileName
NextTargetShape
NextTargatSubFileName
NextTargetSubShape

に設定する。簡単。

 

2枚表示の場合は、

TargetNo
TargetFileName
TargetShape
TargatSubFileName
TargetSubShape

TargetShapeか、TargetSubShapeが両方とも縦長の場合は
進む時、TargetNo+2で、NextTargetを設定する。
これも簡単。


戻る時は、TargetNo-2 では間違い。
形状が重要なのでインデックス1つずつ調べる。

NextTargetNo = TargetNo - 1
NextTargetFileName
NextTargetShape

調査後にNextTargetShapeが縦長なら

val no = NextTargetNo - 1
val path = ファイルリスト[no]
val shape = pathの画像が縦?横?

shapeが縦長なら

NextTargatSubFileName = NextTargetFileName
NextTargetSubShape = NextTargetShape
NextTargetFileName = path
NextTargetShape = path
NextTargetNo = no

shapeが横長なら

NextTargatSubFileName = ""
NextTargetSubShape = -1

で終わり。

 

次は、
TargetNo
TargetFileName
TargetShape
TargatSubFileName
TargetSubShape

TargetShapeか、TargetSubShapeのどちらかが横長の場合は
TargetFileNameの1枚だけを表示している。

TargatSubFileName = ""
TargetSubShape = -1
にしている。


この状況で次を準備するとき

進む時、TargetNo+1で、NextTargetを設定する。

NextTargetNo = TargetNo+1
NextTargetFileName
NextTargetShape

NextTargatSubFileName = ファイルリスト[NextTargetNo+1]
NextTargetSubShape

NextTargetSubShape、NextTargetShape どちらか横なら

NextTargatSubFileName = ""
NextTargetSubShape = -1


前に戻す時、

NextTargetNo = TargetNo - 1
NextTargetFileName = ファイルリスト[NextTargetNo]
NextTargetShape

NextTargetShapeが縦長なら

val no = NextTargetNo - 1
val path = ファイルリスト[no]
val shape = pathの画像が縦?横?

shapeが縦長なら

NextTargatSubFileName = NextTargetFileName
NextTargetSubShape = NextTargetShape
NextTargetFileName = path
NextTargetShape = path
NextTargetNo = no

shapeが横長なら

NextTargatSubFileName = ""
NextTargetSubShape = 横

 

NextTargat情報を元に画像をBitMapに構築する。
そして、情報を更新。

TargetNo = NextTargetNo
TargetFileName = NextTargetFileName
TargetShape = NextTargetShape
TargatSubFileName = NextTargatSubFileName
TargetSubShape = NextTargetSubShape

Targetは、現在スクリーンに表示されている情報。
NextTargatは、進む戻るで準備された次に表示させる情報。


Javaの時は配列のNoに意味付けして配列を渡してたけど、分かりづらかった。
クラスのプロパティにして準備と本配置で区別したので、日にちが経っても思い出しやすい。

 

テストでディレクトリ内の縦横混在画像を想定通りに表示させられたので、
サムネイル表示フラグメントをKotlinに書き直す。

 


  • X
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする