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

android OS & iPadOS の記録。

基礎。作業ディレクトリの考察。分割の前編。

2022-02-16 01:33:44 | Android studio 日記

前回の2つのディレクトリを切り替えも考えつつ説明。


作業ディレクトリが1つの時の動きは。

1、対象ディレクトリのFile情報を取得。
2、作業ディレクトリの作業ファイルを全て削除する。(直前の作業ファイルがある場合)
3、対象ディレクトリの中にあるフォルダアイテムをディレクトリと画像ファイルに分けてList<>化する。

4、画像ファイルからbitmapへデコードする。
5、bitmapをサムネイル画像へ変換する。

6、作業ディレクトリに保存する。


3番のあとにRecyclerViewAdapterで4、5番をしている。
毎回、画像変換すると時間がかかるので、ディレクトリが変更されるまでサムネイル画像を保存して読み出すようにする。

作業場所が1つの場合、作業ファイル削除と画像変換の時間がかかる。
この部分をバックグランドで処理できれば、反応速度が速くなる。

 

作業ディレクトリが2つの時の動きは。
 
1、対象ディレクトリのFile情報を取得。
2、対象の作業ディレクトリを切り替える。
3、前の作業ディレクトリの作業ファイルを全て削除する。(別スレッドでバックグラウンド処理)
4、対象ディレクトリの中にあるフォルダアイテムをディレクトリと画像ファイルに分けてList<>化する。
5、サムネイル画像ファイルがあれば、それを使う。無ければ、リソース画像を表示する。
6、バックグラウンドで画像ファイルからbitmapへデコード、bitmapをサムネイル画像へ変換、作業ディレクトリに保存する。
7、対象ディレクトリの変更の時は、1番からやり直し。

これでFile情報の取得と必要データのリスト化、アダプターへの設定と表の処理時間を減らせた。
画像ファイル数が少なければ、気にならないが数百枚画像があると反応速度にイラつく。


最初のテストでは問題は無かったけど、何か相性が悪い書き込みをしてしまったようで例外が発生するようになった。
色々と対応してみたが原因も突き止められないので、この際だから深く考え直す。

2つの作業ディレクトリで論理的に良いと思うが、低い確率で処理がぶつかって問題になる。
例えば、画像枚数が多くてファイル削除が終わる前にディレクトリ移動が発生した時。
問題回避には看板をかけて処理待ちさせれば良い。低確率だからこれでいいと思うが個人的に納得できない。

となると、作業ディレクトリを必要に合わせて増やせばいい。
そういうクラスを作る。


クラス内で作業ディレクトリを自動で増加させる。
操作可能不可能をできるだけクラス内で自動に設定させる。
別スレッドで作業ファイルを自動で削除させる。

Adapterで使用する作業ディレクトリも内部フラグで自動変更させる。
getTempDirForAdapter() を呼べば、使用中の作業ディレクトリのFile情報が帰る。


次に分割の後編。プログラム草案。

 


基礎。作業ディレクトリの考察。(プログラム草案)

2022-02-16 01:29:31 | Android studio 日記

作業用ディレクトリの管理クラス。

 

    static class MyTempDir {
        private Context mContext = null;
        private ArrayList< File> mTempDir = null;       // 作業ディレクトリ
        private ArrayList< Boolean> usingFlag = null;  // 作業中 true
        private ArrayList< Boolean> usedFlag = null;  // 使用後 true
        private ArrayList< Boolean> clearFlag = null;// 消去中 true

        MyTempDir( Context context ) {
            mContext = context;
            mTempDir = new ArrayList< File>();
            usingFlag =  new ArrayList< Boolean>();
            usedFlag =  new ArrayList< Boolean>();
            clearFlag = new ArrayList< Boolean>();
            init();
        }
        private void init() {
            File dir = new File( mContext.getFilesDir(), "temp" );
            dir = new File( dir, "temp1" );
            boolean b;
            if ( !dir.exists() )  b = dir.mkdirs();
            mTempDir.add(dir);
            usingFlag.add(false);
            usedFlag.add(false);
            clearFlag.add(false);
        }
        private boolean mGetDirFlag = false;
        private File getTempDir() {
            mGetDirFlag = true;
            int usingNo = getUsingTempDir();
            int i = mTempDir.size();
            while ( --i >= 0 ) {
                if ( !usingFlag.get( i ) && !usedFlag.get( i ) && !clearFlag.get( i ) ) break;
            }
            if ( usingNo >= 0 ) {
                usingFlag.set( usingNo, false );
                usedFlag.set( usingNo, true );
            }
            if ( i < 0 ) {
                File dir = new File( mContext.getFilesDir(), "temp" );
                i = mTempDir.size();
                String dirName = "temp" + String.valueOf( (i+1) );
                dir = new File( dir, dirName );
                boolean b;
                if ( !dir.exists() )  b = dir.mkdirs();
                
                mTempDir.add(dir);   // 4つはセットで追加する
                usingFlag.add(true); // 管理できなければ、4つを1つの構造体にして配列使用
                usedFlag.add(false); //
                clearFlag.add(false);//
                
                mGetDirFlag = false;
                allClearTempFile();
                return mTempDir.get(i);
            }
            usingFlag.set( i, true );
            mGetDirFlag = false;
            allClearTempFile();
            return mTempDir.get(i);
        }

        public void setUsingFlag( int no, boolean flag ) { usingFlag.set( no, flag ); }
        public void setUsedFlag( int no, boolean flag ) { usedFlag.set( no, flag ); }
        public void setClearFlag( int no, boolean flag ) { clearFlag.set( no, flag ); }

        public int getUsingTempDir() {
            int i = mTempDir.size();
            while ( --i >= 0 ) {
                if ( usingFlag.get(i) ) break;
            }
            return i;
        }
        private boolean isUsingTempDir( int no ) {
            return ( no >= 0 && no < mTempDir.size() )? usingFlag.get(no): false;
        }
        private boolean isUsedTempDir( int no ) {
            return ( no >= 0 && no < mTempDir.size() )? usedFlag.get(no): false;
        }
        private boolean isClearTempDir( int no ) {
            return ( no >= 0 && no < mTempDir.size() )? clearFlag.get(no): false;
        }

        public void allClearTempFile() {
            int i = mTempDir.size();
            while ( --i >= 0 ) {
                if ( usedFlag.get(i) ) {
                    clearFlag.set( i, true );
                    allClearTempFile( mTempDir.get(i) );
                    clearFlag.set( i, false );
                    usedFlag.set( i, false );
                }
            }

        }
        private void allClearTempFile( File dir ) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while ( true ) {
                        if (!mGetDirFlag) break;
                    }
                    boolean b;
                    File[] files = dir.listFiles();
                    for ( File file: files) {
                        b= file.delete();
                    }
                }
            }).start();
        }

        public File getTempDirForAdapter() {
            int no = getUsingTempDir();
            return ( no < 0 )? null: mTempDir.get( no ); // 使用中の作業ディレクトリを返す
        }
        
        public void finish() {

            mTempDir.clear();
            mTempDir = null;
            usingFlag.clear();
            usingFlag = null;
            clearFlag.clear();
            clearFlag = null;
            mContext = null;
        }
    }


    MyTempDir mTempDir = new MyTempDir( mContext );

 


基礎。作業領域。RecyclerView でサムネイル画像表示。

2022-02-15 01:01:56 | Android studio 日記

キャッシュ領域にサムネイル画像ファイルを配置の場合。
大量の画像を見るとキャッシュディレクトリの容量が400MBを優に超える。

ので、作業ディレクトリを作って、閲覧ディレクトリごとに作業ディレクトリへサムネイル画像ファイルを作る。
流れは、
・作業ディレクトリを2個作る。
・ディレクトリのフォルダアイテム( dir と file )を取得する。
・ソースファイルから加工してテンプファイルを作業ディレクトリに作る。
・別の閲覧ディレクトリに移るとき、別の作業ディレクトリに場所を移す。
・前の作業ディレクトリのファイルを別スレッドで全て消去する。
・閲覧ディレクトリのフォルダアイテムを取得する。(繰り返し)

一つのディレクトリに50枚程度ならキャッシュファイルは必要ないかも。
一時的にリソースのアイコンを表示しておいて、別スレッドでサムネイル画像に変換して後から画像を更新させる。RecycleView の adapter.notifyItemChanged(no); で個別にアイテム更新できる。

 


recyclerView の adapter 設定の一部。
loadImage() でサムネイル画像を確認して bitmap を取得している。

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {

        File souFile = new File( myImageAbsPaths.get(position) );
        Bitmap bitmap = loadImage( souFile );

        if (bitmap != null)
            holder.imageView.setImageBitmap(bitmap);
        else
            holder.imageView.setImageBitmap( BitmapFactory.decodeResource( mContext().getResources(), R.drawable.not_find) );

        holder.textView.setText( String.valueOf( position ) );
        holder.linearLayout.setId( holder.getAdapterPosition() );

    }

 

作業ディレクトリ関連のメソッド。


    private ArrayList< File> mTempDir = new ArrayList< File>();
    private boolean mTempDirFlag = true;

    public void setTempDir( File dir1, File dir2 ) { // 作業前に一度だけ初期化する
        mTempDir.add(dir1);
        mTempDir.add(dir2);
        boolean b;
        if ( !mTempDir.get(0).exists() ) b= mTempDir.get(0).mkdirs();
        if ( !mTempDir.get(1).exists() ) b= mTempDir.get(1).mkdirs();
    }

    public File getTempDir() {
        return (mTempDirFlag)? mTempDir.get(0): mTempDir.get(1);
    }

    public void switchTempDir() {
        mTempDirFlag = !mTempDirFlag;
        new Thread(new Runnable() {
            @Override
            public void run() {
                allDeleteFiles();
            }
        }).start();
    }

    public void allDeleteFiles(){
        int sw = (mTempDirFlag)? 1: 0;

        File[] files = mTempDir.get(sw).listFiles();
        if ( files == null || files.length == 0 ) return;

        boolean b;
        for (File file : files) {
            b = file.delete();
        }
    }

/////// UI スレッド上に用意しておいて、別スレッドから呼び出す。

    public void adapterNotifyItemChanged( int no ) {
        mRecyclerView.post(new Runnable() {
            @Override
            public void run() {
                RecyclerViewAdapter adapter = (RecyclerViewAdapter) mRecyclerView.getAdapter();
                if (adapter != null) adapter.notifyItemChanged(no);
            }
        });
    }

/////// スレッド内に直接配置でも動いたが表示に変な挙動が表れた。

 

 

Runnableの一部分。実際に動かすには、UIスレッドとの情報のやり取りが必要。引数に構造体を渡す工夫。


public class MyRunnable implements Runnable {
    private List< String> mFileNameList = null;
    private List< String> mFileAbsPathList = null;
    private File mTargetDir = null;

    MyRunnable( File dir ) {
        mTargetDir = dir;
    }

    @Override
    public void run() {
        if ( mTargetDir == null || !mTargetDir.exists() || !mTargetDir.isDirectory() ) return;
        
        mFileNameList = new ArrayList<>();
        mFileNameList.addAll( getFileNameListForAdapter() );

        mFileAbsPathList = new ArrayList<>();
        mFileAbsPathList.addAll( getAbsolutePathListForAdapter() );


        File tempDir = getTempDir();
        File tempFile, souFile;
        for ( int i = 0; i < mFileNameList.size(); i++ ) {
            tempFile = new File( tempDir, mFileNameList.get(i) );
            if ( tempFile.exists() ) continue;

            souFile = new File( mFileAbsPathList.get(i) );
            bitmapToThumbnaile( souFile, tempFile );
            adapterNotifyItemChanged(i); // 外部のアダプタアイテム更新を呼ぶ
        }


        finish();
    }


    private void bitmapToThumbnaile( File souFile, File destfile ) {

        Bitmap bitmap = thumbnaileImage( souFile );
        if ( bitmap != null ) {
            try {
                FileOutputStream fos = new FileOutputStream( destfile );
                Bitmap.CompressFormat bcf = getCompressFormat( souFile.getName() );
                if ( bcf != null ) bitmap.compress( bcf, 100, fos );
                fos.close();
            } catch (IOException e) {
                return;
            }
            if ( !bitmap.isRecycled() ) bitmap.recycle();
        }
        bitmap = null;
    }

 

一部を抜粋。

 

 

 


草案。recyclerView でサムネイル画像表示。操作感をもっと速く。

2022-02-07 23:20:45 | Android studio 日記

現状プログラムは、

・recyclerView のアダプターに画像設定するためにキャッシュディレクトリにサムネイル画像を用意する。
・キャッシュに画像が無ければリソースの画像を代わりに表示させておく。
・後で別スレッドでキャッシュにサムネイル画像を作成する。
その結果、前よりはサムネイル表示モードでは速く表示できた。

しかし、ディレクトリ移動モードのサムネイル画像表示では速くできなかった。
バックグラウンドでデータを作成するならと、余計な情報を作って利用したためか。
これが検索範囲を広げてしまい遅くなった?。
なので、ディレクトリ内のフォルダアイテムを単純にリスト化して、キャッシュデータ作成、利用とした。
なぜか表示が上手くいかない。Thread が起動する時と起動しないときが起こる。よく分からない。 *

キャッシュディレクトリに大容量の領域を使うのは良くないので、新たに作業領域という考えを加えてみる。
1つのディレクトリのフォルダアイテムを取り扱い、ディレクトリが移動したら作業領域を初期化して、新規フォルダアイテムの情報を新構築する。
ページ表示で先ずはリソースの画像を表示して、後からレイアウトマネージャーのViewを取得してサムネイル画像を設定する。(別スレッドはちゃんと起動するかなぁ)

どうなんだろう?
アプリ固有のデータフォルダにサムネイル作成は、アプリ削除で一緒に消えるからキチンと後始末していれば問題ない。
だけど、面倒くさいな…。

 

追記:*部分はスレッド内でクラス外メソッドを呼んで数値を参照していた。そのメソッド名が似ていて間違った方を呼んでしまって分岐が意図しない方へ飛んでいた。


基礎。touchアクション

2022-02-01 14:15:53 | Android studio 日記

ボタンなどタッチした時に画像など変化が起きるとタッチした事が分かりやすい。
変化を加えるメソッドを考える。(一番楽な方法)

アニメーションが少し分かったので、view.startAnimation() を利用する。
View.OnClickListener() でイベント発生すると view が渡されるので、その view にアニメーションを施す。

 

ここでは透明度を変化させている。大きさを変化させれば、よく見るアニメーション。

【fade_in_animation_set.xml】

< ?xml version="1.0" encoding="utf-8"?>
< set xmlns:android="http://schemas.android.com/apk/res/android"
    android:shareInterpolator="@android:anim/decelerate_interpolator">

    < alpha
        android:fromAlpha="0"
        android:toAlpha="1.0"
        android:fillAfter="true"
        android:duration="650" />
< /set>

 

 

アクション実体部分。

    private final Handler mHandler = new Handler();
    public void touchAction( View view ) {
        mHandler.post( new Runnable() {
            @Override
            public void run(){
                view.startAnimation( AnimationUtils.loadAnimation( mContext, R.anim.fade_in_animation_set) );
            }
        } );
    }

 

 

クリックイベントにアクションメソッドを組み込む。

    mButtonClick = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            touchAction(v); // アクションを呼ぶ
            // ボタン処理
        }
    };

直ぐに場面が変わる時は、変化アニメーションが表示されない。
それが気に入らなければ、遅延処理を組み込んで次の処理をアニメーション終了後に始める。

    mButtonClick = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            touchAction(v);
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run(){
                // 処理メソッドを呼ぶ
                }
            }, 800 );
        }
    };

 

image viewer の実機テスト(android OS4.2)でサムネイル画像スクロールはキャッシュ利用で速く表示された。
しかし、ディレクトリ移動モードではタッチから次のディレクトリ内容表示まで時間が遅い。
データ収集の無駄を省き処理時間の短縮を図るがまだ足りない。
そんなに時間がかかる処理には思えないが…。
情報再利用の構造体を構築し、表示データ作成を省く考えが必要か?
情報構造体のデータ収集を別スレッドで行う。
その範囲を決めて自動で収集。
別スレッドの始まりと終わり。
トリガー。保存。運用。などなどを考える。