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

android OS & iPadOS の記録。

基礎。AsyncTask()、1枚のサムネイル画像の作成保存。

2022-06-30 14:05:14 | Android studio 日記

一枚のサムネイル画像を作成して、キャッシュディレクトリに保存する。

public class MyAsyncTask extends AsyncTask< String,Object,Object > {

    private MyMainData mMainData;
    private RecyclerView mRecyclerView;
    private File mCacheDir;
    private final int vID;
    private final static int SAVE_SIZE = 160;

    public MyAsyncTask( MyMainData data, RecyclerView rv, int id ) {
        super();
        mMainData = data;
        mRecyclerView = rv ;
        vID = id;
        mCacheDir = data.getCacheDir(true); // キャッシュディレクトリ取得
    }

    // メインスレッドで実行
    @Override
    protected void onPreExecute() {
        // 必要な(前)処理
    }

    // 別のスレッドで実行
    @Override
    protected Object doInBackground(String... p) { // MyTask.execute(引数);で呼ばれる
        File souFile = new File(p[0]);
        if ( !souFile.exists() || !souFile.isFile() ) return false;
        if ( !mCacheDir.exists() && !mCacheDir.mkdirs() ) return false;

        File cacheFile = new File( mCacheDir, souFile.getName() );
        if ( cacheFile.exists() ) return false; // キャッシュファイルが無いからここに来ているけど..

        return bitmapToThumbnaile(souFile, cacheFile);
    }

    private boolean 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, 85, fos );
                fos.close();
            } catch (IOException e) {
                return false;
            }
            if ( !bitmap.isRecycled() ) bitmap.recycle();
        }
        return true;
    }

    private Bitmap thumbnaileImage( File souFile ) {
        Bitmap scaleBitmap = null;
        try {
            BitmapFactory.Options op = new BitmapFactory.Options();
            op.inJustDecodeBounds = true;
            InputStream istream = new FileInputStream( souFile );
            BitmapFactory.decodeStream( istream, null, op );
            op.inSampleSize = calculateInSampleSize( op );
            op.inJustDecodeBounds = false;
            istream.close();

            istream = new FileInputStream( souFile );
            scaleBitmap = BitmapFactory.decodeStream( istream, null, op );
            istream.close();
            if ( scaleBitmap == null ) return null;

            Matrix matrix = new Matrix();
            float mag = calcMag( scaleBitmap.getWidth(), scaleBitmap.getHeight() );
            matrix.postScale( mag, mag );
            scaleBitmap = Bitmap.createBitmap( scaleBitmap, 0, 0, scaleBitmap.getWidth(), scaleBitmap.getHeight(), matrix, true);
        } catch (OutOfMemoryError | IOException ignored) {
            return null;
        }
        return scaleBitmap;
    }

    private int calculateInSampleSize(BitmapFactory.Options op) {
        int height = op.outHeight;
        int width = op.outWidth;
        int inSamplSize = 1;
        int border = SAVE_SIZE;

        if ( height > border || width > border) {
            int halfHeight = height / 2;
            int halfWidth = width / 2;

            while ( ( halfHeight / inSamplSize ) >= border &&
                    ( halfWidth / inSamplSize ) >= border
            ) {
                inSamplSize *= 2;
            }
        }
        return inSamplSize;
    }

    private Bitmap.CompressFormat getCompressFormat(String name ) {
        String t = name.toLowerCase();
        if ( t.endsWith( ".jpg" ) ) return Bitmap.CompressFormat.JPEG;
        if ( t.endsWith( ".png" ) ) return Bitmap.CompressFormat.PNG;
        return null;
    }

    private float calcMag( int souX, int souY ) {
        float magW = (float)SAVE_SIZE / (float)souX;
        float magH = (float)SAVE_SIZE / (float)souY;

        return Math.min(magW, magH);
    }


    // 途中経過、メインスレッドで実行
    @Override
    protected void onProgressUpdate(Object... o) {
        // 経過表示
    }

    // 終了後、メインスレッドで実行
    @Override
    protected void onPostExecute( Object result ) {
        if ( (boolean)result ) {
            if (Objects.equals(mRecyclerView.getAdapter(), mMainData.getThumbnailPage().getRecyclerView().getAdapter()))
                // アダプターが更新されていない時
                Objects.requireNonNull(mRecyclerView.getAdapter()).notifyItemChanged(vID);
        }

        finish();
    }
    private void finish() { // 念のため参照切り
        mCacheDir = null;
        mRecyclerView = null;
        mMainData = null;
    }
}


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

基礎。AsyncTaskを使ったRecyclerView.Adapter。

2022-06-30 13:33:33 | Android studio 日記

AsyncTaskを使うことでシンプルな表記になった。
サムネイル画像がキャッシュファイルになっていなければ、AsyncTaskで作成保存する。
AsyncTaskは、1枚単位で実行。

ファイルリストデータ更新をRecyclerView.setAdapter(adapter) で丸ごと変更とし、旧データ放棄はシステムに任せる。

トリガーは任意の場所で、setAdapter() を実行する。

    mRecyclerView.setAdapter( new MyThumbnailAdapter2( mMainData ) );


一部省略。
public class MyThumbnailAdapter2 extends RecyclerView.Adapter< MyThumbnailAdapter2.ViewHolder > {

    private MyMainData myMainData = null;
    private LayoutInflater myInflater = null;
    private RecyclerView myRecyclerView = null;
    private View.OnClickListener myListener = null;
    private List myImageAbsPaths = null;
    private File myCacheDir = null;

    public MyThumbnailAdapter2( MyMainData mainData ) {
        myMainData = mainData;
        myCacheDir = mainData.getCacheDir(true);
        myInflater = LayoutInflater.from( mainData.getContext() );

        // メインデータクラスからファイルリストをコピーする
        if ( mainData.getMainDirData() != null && mainData.getMainDirData().getFileListSize() > 0 )
            myImageAbsPaths = new ArrayList<>( mainData.getMainDirData().getAbsolutePathListForAdapter() );

    }

    @Override
    public void onAttachedToRecyclerView( @NonNull RecyclerView recyclerView ) {
        super.onAttachedToRecyclerView( recyclerView );
        myRecyclerView = recyclerView;
    }
    @Override
    public void onDetachedFromRecyclerView( @NonNull RecyclerView recyclerView ) {
        myRecyclerView = null;
        super.onDetachedFromRecyclerView( recyclerView );
    }

    @NonNull
    @Override
    public MyThumbnailAdapter2.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = myInflater.inflate( R.layout.thumbnail_view, parent, false );
        return new MyThumbnailAdapter2.ViewHolder( view );
    }

    @Override
    public void onBindViewHolder(@NonNull MyThumbnailAdapter2.ViewHolder holder, int position) {
        if ( myImageAbsPaths == null || getItemCount() <= 0 ) return;

        File file = new File( myImageAbsPaths.get(position) );
        Bitmap bitmap = loadImage( file );
        if (bitmap != null)
            holder.imageView.setImageBitmap(bitmap);
        else {
            holder.imageView.setImageBitmap(
                  BitmapFactory.decodeResource(myMainData.getContext().getResources(), R.drawable.img_file)
            );

            MyAsyncTask mAsyncTask = new MyAsyncTask( myMainData, myRecyclerView, position );
            mAsyncTask.execute( myImageAbsPaths.get(position) );

        }

        holder.textView.setText( String.valueOf( position + 1 ) );
        initStartPositionOffset( holder.linearLayout, position );
        holder.linearLayout.setId( holder.getAdapterPosition() );
        holder.linearLayout.setOnClickListener( (View v) -> { myListener.onClick(v); } );
    }

    public Bitmap loadImage( File file ) {
        if ( file == null || !file.exists() || !file.isFile() ) return null;

        Bitmap scaleBitmap = null;
        File cf = new File( myCacheDir, file.getName() );
        if ( cf.exists() )
            scaleBitmap = BitmapFactory.decodeFile(cf.getAbsolutePath());

        return scaleBitmap;
    }

    public void initStartPositionOffset(View view, int position) {
        省略
    }

    public void setOnItemClickListener( View.OnClickListener listener ) { myListener = listener;}

    @Override
    public int getItemCount() { return myImageAbsPaths.size();}

    static class ViewHolder extends RecyclerView.ViewHolder {
        final LinearLayout linearLayout;
        final ImageView imageView;
        final TextView textView;

        ViewHolder(View v) {
            super(v);
            linearLayout = v.findViewById( R.id.thumbnail_Linear_layout );
            imageView = v.findViewById( R.id.thumbnail_imageview );
            textView = v.findViewById( R.id.thumbnail_textview );
        }
    }

}

 


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

再考。RecyclerView。

2022-06-25 11:22:02 | Android studio 日記

AsyncTaskを使うのでトリガーとなるrecyclerView周りを調べる。
デベロッパーでも一般サイトでもRecyclerView.setAdapter( adapter )での説明に移っている。
以前は見なかったので追加された?

デベロッパーの説明では、プールにadapterが無ければ旧adapterを廃棄して新adapterをセットする。
これは楽だ。

データの総入れ替えはレイアウトマネージャーとアダプターをクリアして、新しいデータをアダプターに設定していた。
初回設定の仕方はどこにもサイトも説明しているが、サイズの違うデータの総入れ替えの説明は見つからなかった。さらにシステムの内部動作が分からないから念のためクリアを組み込んだ。

デベロッパーで見つけたsetAdapter()が旧adapterを廃棄する。と明記しているので、関係レイアウトマネージャーも上手いことやってくれると推測した。

じゃぁ。という事で自前クリアを注釈変更して、setAdapter()を組み込む。
あれ?recyclerViewの画像表示がされない…。スクロールするとサムネイル画像が出てくる^^;
notifyDataSetChanged()、setNotifyItemChanged()、の使いどころだろうか。

AsyncTaskと一緒にRecyclerViewを作り変える。

 


今回のテストでビルドエラーは出ない。
エミュレーターはAPI17で動作テスト。
recyclerviewの初期表示のみ不具合だった。

<追記>
recyclerViewの初期表示に不具合が発生したのは、テスト用のキャッシュファイルの作成タイミングと場所に原因があった。本番用と違っている内容だった。
今後は内部データディレクトリと外付けストレージのキャッシュファイルを分けて管理するので、テスト用と本番用は同じもので組む。
また、機能、構造をシンプルにする。

RecyclerView.setAdapter( adapter )は、始めた当初から使っていた(笑)mainActivityのみで。
adapter.addAll()でデータ更新していたからsetAdapter()を忘れていた^^;。

 


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

基礎。スレッドプール考察。

2022-06-17 17:01:20 | Android studio 日記

わからん。
スレッドをキューに溜め込んでいって順番にバックグラウンドで処理をしている。
大雑把に言うと。

その方法の種類が複数ある。
developerは、分かっている人向けの説明で、かつ、抽出説明だから理解できない。

Executorsクラスがあって、その下に複数の継承クラスがある。
developerの説明はホント理解しづらい。


//developer引用。//

public class MyApplication extends Application {
    ExecutorService executorService = Executors.newFixedThreadPool(4);
    executorService.submit( ここに何かを入れる );この表記&説明は無い。上の行のみの記載だった。
}


public class MyApplication extends Application {
    /*
     * Gets the number of available cores
     * (not always the same as the maximum number of cores)
     */
    private static int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();

    // Instantiates the queue of Runnables as a LinkedBlockingQueue
    private final BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>();

    // Sets the amount of time an idle thread waits before terminating
    private static final int KEEP_ALIVE_TIME = 1;
    // Sets the Time Unit to seconds
    private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;

    // Creates a thread pool manager
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            NUMBER_OF_CORES,       // Initial pool size
            NUMBER_OF_CORES,       // Max pool size
            KEEP_ALIVE_TIME,
            KEEP_ALIVE_TIME_UNIT,
            workQueue
    );
    ...
}

//developer引用ここまで。//


Executors
ExecutorService
ThreadPoolExecutor

これらでスレッドを溜め込んで処理できる。

 

**スレッドプールを使いたい理由**
ディレクトリ内の画像をサムネイル作成&キャッシュファイル保存はスレッド処理では数十枚が限界。
数百枚だとサムネイル表示の待ち時間が長すぎる。

なので、全体を順番に処理するスレッドとrecyclerViewのスライドに合わせて見えている部分を処理するスレッドを同時に動かしたい。
だけど、developerの説明はわからん。ネットワーク処理説明が優先だし。

 

色々検索して、こちらでスレッドプールの説明を読みました。

https*//outofmem.tumblr.com/post/94053254909/android-executor-1
(*→:)

まとめで、AsyncTaskでダメな場合は、そちらを考えるのでもいいんじゃないか。という解説でした。


なんか、AsyncTaskもスレッドプールの機能が備わっているらしい。
API29までの限定アプリにしてしまえという事で、AsyncTaskの封印を解く。


並列処理なのでキャッシュファイル保存で問題が起きるはず。
保存前チェックと破棄、例外処理はキッチリ考える。

また、閲覧場所を固定したので内部ディレクトリは固有キャッシュディレクトリにサムネイルファイルを保存する。
/Removable のディレクトリは、元画像のディレクトリ内にthumbnaile-testディレクトリを作り、そこに保存する。

こんな感じ。

 

 


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

基礎。複数の指定ディレクトリの切り替え。

2022-06-11 15:41:54 | Android studio 日記

android studio アップデートされました。2021.2.1 なんのトラブルもなく良かった。

ディレクトリのアクセスについて。

新しいOSほどセキュリティの都合で“File.listFiles()”を使い、自由にアイテムを取得できないから、
アクセスの許されたディレクトリを設定して、その親ディレクトリへの移動を禁止させて運用する。

OS毎に対応も面倒だし、閲覧場所を固定してボタンで切り替え…。
まぁ仕方ない。

 

内容
・設定の基本ディレクトリの中では自由に移動できる事。
・親ディレクトリへ移動の前に毎回ディレクトリの検査を行い、移動禁止判定を下す事。

 

public class MyDirectory {

    private ImageView mPicturesButton = null; // 設定は省略。タップ処理設定も省略。
    private ImageView mDcimButton = null;
    private ImageView mRemovableButton = null;// MainActivity で設定する体で

    private View.OnClickListener mPicturesButtonClick = null;
    private View.OnClickListener mDcimButtonClick = null;
    private View.OnClickListener mRemovableButtonClick = null;

 

    private final Handler mHandler = new Handler();// ボタンアニメーション用

    MyDirectory( MyMainData mainData ) {

        mPicturesButtonClick = new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if ( mMainData.isStandBy() ) return;// 処理中に再度タップされたら戻る
                mMainData.setStandBy(true);// 処理中を設定
                mMainData.touchActionB( v );// アニメーション実行

                mHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        changeDirectoryBase(0);
                        mMainData.setStandBy(false);// 処理完了
                    }
                }, 400);// アニメーションが終わった頃に実行する
            }
        };
        mDcimButtonClick = new View.OnClickListener() { 省略 };
        mRemovableButtonClick = new View.OnClickListener() { 省略 };

    }


    private void changeDirectoryBase( int no ) {
        if ( mMainData.getTargetDirectoryBase() == null ) return;

        File dir;
        switch ( no ) {
            case 1:
                dir = getExternalStoragePublicDirectory( DIRECTORY_DCIM );
                break;
            case 2:
                dir = new File("/Removable");
                break;
            default:
                dir = getExternalStoragePublicDirectory( DIRECTORY_PICTURES );
        }
        if ( mMainData.isEqualBase( dir ) ) return;

        if ( setDirectoryList( dir ) == 0 )// **
            mMainData.setTargetDirectoryBase( dir );
    }
/*

** setDirectoryList()は設定ディレクトリボタンをタップしたときに新たなファイルアイテムを取得するメソッド。
正常に終了したら、0を返す。そして、基本ディレクトリ情報をセットする。

*/

 

    public void setPicturesButtonView(ImageView Pic) { mPicturesButton = Pic; }
// mDcimButton、mRemovableButton 省略

    public ImageView getPicturesButtonView() { return mPicturesButton; }
// mDcimButtonClick、mRemovableButton 省略

    public View.OnClickListener getPicturesButtonClickListener() { return mPicturesButtonClick; }
// mDcimButton、mRemovableButtonClick 省略

}

 

他のクラスで参照しやすいようにまとめる。

・現在対象の基本ディレクトリを保存。
・表示ディレクトリが基本ディレクトリか調べる。
・表示ディレクトリ、対象アイテムは、どの設定されている基本ディレクトリの系列か調べる。

public class MyMainData {

    private Context mContext;
    MyMainData(Context c) {
        mContext = c;
        // 必要な処理を書く
    }
    private boolean mAllWaitFlag = false;

    public boolean isStandBy() { return mAllWaitFlag; }
    public void setStandBy( boolean flag ) { mAllWaitFlag = flag; }


    private File mTargetDirectoryBase = null;

    public File getTargetDirectoryBase() { return mTargetDirectoryBase; }

    public boolean setTargetDirectoryBase( File dir ) {
        if ( !dir.exists() ) {
            mTargetDirectoryBase = getExternalStoragePublicDirectory(DIRECTORY_PICTURES);
            return false;
        }

        mTargetDirectoryBase = dir;
        return true;
    }

    public boolean isEqualBase( File dir ) {
        if ( !dir.exists() || !dir.isDirectory() ) return false;
        return isEqualBase( dir.getAbsolutePath() );
    }

    public boolean isEqualBase( String path ) {
        return path.equals( mTargetDirectoryBase.getAbsolutePath() );
    }


    public File isContainsPath( File dir ) {
        if ( !dir.exists() || !dir.isDirectory() ) return null;
        return isContainsPath( dir.getAbsolutePath() );
    }

    public File isContainsPath( String path ) {
        File dir = getExternalStoragePublicDirectory( DIRECTORY_PICTURES );
        if ( path.startsWith( dir.getAbsolutePath() ) ) return dir;

        dir = getExternalStoragePublicDirectory( DIRECTORY_DCIM );
        if ( path.startsWith( dir.getAbsolutePath() ) ) return dir;

        if ( path.startsWith( "/Removable" ) ) return new File("/Removable");

        return null;
    }

    private final Handler mHandler1 = new Handler();
    private final Handler mHandler2 = new Handler();

    public void touchActionB( View view ) {
        mHandler1.post( new Runnable() {
            @Override
            public void run(){
                view.startAnimation( AnimationUtils.loadAnimation( mContext, R.anim.scale_down_animation_set) );
            }
        } );
        mHandler2.postDelayed( new Runnable() {
            @Override
            public void run(){
                view.startAnimation( AnimationUtils.loadAnimation( mContext, R.anim.scale_up_animation_set) );
            }
        },160 );// mHandler1が終わった頃に開始

    }
}

public class MainActivity extends AppCompatActivity {

    private MyMainData mainData;
    private MyDirectory mDirectory;

省略
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

色々省略
        mainData = new MyMainData( this );
        mDirectory = new MyDirectory(mainData);

        ImageView iv = findViewById(R.id.pictures_button);// activity_main.xml設定は説明省略

        mDirectory.setPicturesButtonView( iv );
        iv.setOnClickListener(mDirectory.getPicturesButtonClickListener());

        // 他のボタン省略

    }
}

 

 


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