5.13.1. Приложение "Галерея" (Часть 2)

5.13.1. Приложение "Галерея" (Часть 2)

Файлы ресурсов готовы, настало время изменения кода. Создайте новый класс с названием FileAdapter и измените его код таким образом:

// Класс адаптера для привязки и показа данных в RecyclerView
public class FileAdapter extends RecyclerView.Adapter<FileAdapter.ViewHolder> {
    // Локальный массив для хранения файлов
    private List<File> items;
    // Ассоциативный массив (словарь) - массив с ключами и значениями, в отличии от обычного массива в котором ключи это числовые индексы позиции элементов, в HashMap ключом может быть объект любого класса
    private HashMap<String, Bitmap> thumbs = new HashMap<>();
    // Контекст в котором заружен RecyclerView к которому прикреплен этот адаптер
    private Context context;
    // По значению этой переменной определяется генерация миниатюр для изображений или видео
    int type = 0;

    // Функция для обновления списка для показа новых добавленных файлов
    public void UpdateDataSet(File[] items) {
        // Обновить массив, используя конвертацию обычного массива в ArrayList
        this.items = new ArrayList<>(Arrays.asList(items));
        // Вызвать функцию для привязки элементов из нового источника к RecyclerView
        notifyDataSetChanged();
    }

    // Вложенный класс класса FileAdapter для хранения объектов элементов RecyclerView
    static class ViewHolder extends RecyclerView.ViewHolder {
        // Ссылка на TextView в котором выводится заголовок файла
        TextView title;
        // Ссылка на TextView для вывода размера файла
        TextView size;
        // Ссылка на ImageView для показа миниатюры изображения или видео
        ImageView thumb;

        ImageButton rename;
        ImageButton delete;

        // Конструктор класса для присвоения переменным соответствующих объектов из файла макета
        ViewHolder(View itemView) {
            // Необходимо вызвать суперкласс ViewHolder-а и передать корневой View
            super(itemView);
            // Получить ссылку к виджетам в корневом View
            title = itemView.findViewById(R.id.fileTitle);
            thumb = itemView.findViewById(R.id.thumb);
            size = itemView.findViewById(R.id.size);
            rename = itemView.findViewById(R.id.rename);
            delete = itemView.findViewById(R.id.delete);
        }
    }

    // Конструктор класса FileAdapter где получаются значения
    // из вызываюшего кода и присваиваются локальным переменным класса
    FileAdapter(Context context, File[] items, int type) {
        this.items = new ArrayList<>(Arrays.asList(items));
        this.context = context;
        this.type = type;
    }

    // Функция onCreateViewHolder вызывается при первом создании объекта для хранения элементов списка
    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        // Получаем View объект созданный на основе написанной структуры макета в xml файле file_item.xml
        View v = LayoutInflater.from(context).inflate(R.layout.file_item, parent, false);
        // Создаётся новый объект класса ViewHolder и ему передаётся объект View полученный из файла макета
        return new ViewHolder(v);
    }

    // Функция вызывается при присвоении значений переменным класса ViewHolder
    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, final int position) {
        holder.title.setText(items.get(position).getName());
        holder.size.setText(String.format(Locale.ROOT, "Размер: %d Кб", (int) ((float) items.get(position).length() / (float) 1024)));
        if (thumbs.containsKey(items.get(position).getAbsolutePath())) {
            holder.thumb.setImageBitmap(thumbs.get(items.get(position).getAbsolutePath()));
        } else {
            holder.thumb.setImageDrawable(context.getResources().getDrawable(R.drawable.placeholder));
            GenerateThumb gt = new GenerateThumb();
            // Запустить поток для генерации миниатюр для изображений или видео
            gt.execute(items.get(position).getAbsolutePath(), holder.thumb);
        }

        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent fullScreen = new Intent(context, FullScreenViewActivity.class);
                fullScreen.putExtra("type", type);
                fullScreen.putExtra("path", items.get(position).getAbsolutePath());
                context.startActivity(fullScreen);
            }
        });

        // Установить обработчик нажатия на кнопку редактирования объекта
        holder.rename.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // Инициализация диалогового окна для переименования файла
                RenameDialog renameDialog = new RenameDialog();
                // Объект для хранения параметров для передачи в диалоговое окно переименования файла
                Bundle args = new Bundle();
                // Добавить название файла и позицию файла с ключами name и position в парметры
                // передаваемые в диалоговое окно переименования файла
                args.putString("name", items.get(position).getName());
                args.putInt("position", position);
                // Установить аргументы для передачи
                renameDialog.setArguments(args);
                // Указать ссылку на адаптер, для получения доступа к адаптеру через диалоговое окно переименования файла
                renameDialog.adapter = FileAdapter.this;
                // Показать диалоговое окно для переименования файла
                renameDialog.show(((AppCompatActivity) context).getSupportFragmentManager(), "RenameDialog");
            }
        });

        // Установить обработчик клика на кнопку удалить
        holder.delete.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // Заключить попытку аделния файла в блок try catch для перехвата ошибок и предотвращения сбоев в работе приложения
                try {
                    // Объявить временную переменную для хранения выбранного для удаление файла
                    File f = new File(items.get(position).toURI());
                    // Функция File.delete() возвращает true при удалении файла без ошибок, здесь проверяется удалился ли файл, если да тогда...
                    if (f.delete()) {
                        // ... удалить объект из массива
                        items.remove(position);
                        // сообщить адаптеру что объект удален, нужно для удаления объекта из списка  и анимации
                        notifyItemRemoved(position);
                        // сообщить адаптеру что объекты в указанном диапазоне изменились, нужно для корректировки новых позиции элементов
                        notifyItemRangeChanged(position, getItemCount());
                        // сообщить пользователю что файл удален
                        Toast.makeText(context, "Файл удален", Toast.LENGTH_SHORT).show();
                    } else {
                        // Если вызывается эта часть кода значит фалй не был удален
                        Toast.makeText(context, "Файл не удалён", Toast.LENGTH_SHORT).show();
                    }
                    // В случае возникновения ошибок перехватить их и обработать здесь
                } catch (Exception e) {
                    // Вывод в консоль информацию о возникшей ошибке
                    e.printStackTrace();
                }
            }
        });

    }

    // Функция для переименования файла
    public void renameFile(String newName, int position) {
        // Создать новый пустой файл с новым именем переименованного файла
        File file = new File(items.get(position).getParentFile().getAbsolutePath() + "/" + newName);
        // Переименовать выбранный файл и присвоить новое имя
        if (items.get(position).renameTo(file)) {
            // Если получилось переименовать тогда удалить из массива старый файл
            items.remove(position);
            // Добавить новый файл на место удаленного файла
            items.add(position, file);
            // Сообщить адаптеру что инфрмация изменилась
            notifyItemChanged(position);
        }
    }

    // Получить количество элементов в списке
    @Override
    public int getItemCount() {
        return items.size();
    }

    // Поток для генерации миниатюр изображений
    class GenerateThumb extends AsyncTask<Object, Bitmap, Bitmap> {
        // Ссылка на ImageView в котором необходимо показать сгенерированную миниатюру
        ImageView iv;

        // Функция выполняется в фоновом режиме, чтобы приложение не зависло при генерации миниатюр
        @Override
        protected Bitmap doInBackground(Object... params) {
            // Присвоить локальной переменной ImageView ссылку на ImageView для которой генерируется миниатюра
            iv = (ImageView) params[1];
            // Получить путь к файлу на основе которого генерируется миниатюра
            // Путь передан через параметры во время запуска потока
            String path = params[0].toString();
            // Переменная для хранения миниатюры
            Bitmap thumb;
            // Если type == 0 значит надо сгенерировать миниатюру для изображения
            if (type == 0) {
                // Создать объект Bitmap на основе пути к файлу
                Bitmap bitmap = BitmapFactory.decodeFile(path);
                // Присвоить временной переменной значение сгенерированного Bitmap-a размером 256 на 256 пикселей
                thumb = ThumbnailUtils.extractThumbnail(bitmap, 256, 256);
                // Иначе сгенерировать миниатюру для видео
            } else {
                // Сгенерировать миниатюру для видео и присвоить временной переменной, размер миниатюры поставить системным рамзером 512 на 384 пикселей, потому что для миниатюр есть два размера которые указываются через константы, это MINI_KIND и MICRO_KIND 96 на 96 пикселей
                thumb = ThumbnailUtils.createVideoThumbnail(path, MediaStore.Video.Thumbnails.MINI_KIND);
            }
            // Добавить миниатюру в HashMap, указав ключом путь к файлу и значением объект Bitmap
            thumbs.put(path, thumb);
            // publishProgress выполняется в главном потоке и служит для обновления пользовательского интерфейса, который вызывается из потока выполнемого в фоновом режиме, так как поток фонового режима не имеет возможности изменять объекты View
            publishProgress(thumb);
            // Значение return в данном случае не исользуется и если необходимо использовать надо дописать функцию onPostExecute который и принимает в качестве параметра значения рассчитанные или обработанные в фоновом потоке
            return null;
        }

        // Функция вызывается для обновления пользовательского интерфейса в промежутках обработки фонового потока если не вызывать эту функцию то пользователю придется ждать пока все миниатюры не будут сгенерированы и только потом показать их, а вызывая эту функцию при генерации каждой миниатюры они постепенно появляются уже в пользовательском интерфейсе
        @Override
        protected void onProgressUpdate(Bitmap... values) {
            super.onProgressUpdate(values);
            // Устанавливает в ImageView сгенерированную миниатюру
            iv.setImageBitmap(values[0]);
        }
    }
}

 Создайте ещё один класс и назовите MediaList и напишите следующий код:

public class MediaList extends Fragment {
    RecyclerView fileList;
    FileAdapter adapter;
    GridLayoutManager glm;
    File base;
    String path;
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        // Получить через файл макета RecyclerView и присвоить локальной переменной
        fileList = (RecyclerView) inflater.inflate(R.layout.media_list_fragment, container, false);

        // Получить переданный путь и присвоить локальной переменной
        path = getArguments().get("path").toString();

        // Создать экземпляр GridLayoutManager для показа содержимого RecyclerView в виде плитки
        glm = new GridLayoutManager(getContext(), 2);
        // Присвоить экземпляру RecyclerView экзмепляр класса GridLayoutManager
        fileList.setLayoutManager(glm);

        // Вызов функции для установки адаптера для RecyclerView
        setAdapter();
        // Вернуть полученный через LayoutInflater RecyclerView в качестве объекта View для показа в Activity
        return fileList;
    }

    public void setAdapter() {
        // Переменная для хранения объекта класса File, который хранит в себе путь к папке с изображениями и видео
        base = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "My Lesson Camera/" + path);

        // Проверить существует ли корневая папка
        if (!base.exists()) {
            // Если папка не существует что может быть при первой установке программы создать эти папки
            base.mkdirs();
        }
        // Создать адптер, куда передаётся путь к файлам и параметр со значением 0 если надо создать миниатюры для файлов в папке Photos
        // и 1 если надо сгенерировать миниатюры для файлов в папке Videos
        adapter = new FileAdapter(getContext(), base.listFiles(), path.equals("Photos") ? 0 : 1);
        // Установить адаптер для показа элементов в списке
        fileList.setAdapter(adapter);
    }

    // Функция служит для обновления списка с файлами, которая вызывается из MainActivity при съемке новых фото и видео
    public void UpdateAdapter() {
        // Вызвать функцию UpdateDataSet из адаптера для передачи списка файлов вместе с новыми файлами для обновления списка
        adapter.UpdateDataSet(base.listFiles());
    }

}

Добавьте новый класс, на этот раз это класс для показа диалогового окна переименования файла, назовите новый класс DialogFragment:

public class RenameDialog extends DialogFragment {
    // Объявление переменных для хранения ссылки на экземпляр адаптера, целое значение позиции названия файла и расширения файла, которое получаем из названия
    public FileAdapter adapter;
    public int position;
    public String name;
    public String extension = "";

    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // Объявление экземпляра AlertDialog.Builder для создания диалогового окна
        AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
        // Получение LayoutInflater-a
        LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        // Присвоить переменной View макет загруженный через LayoutInflater
        View v = inflater.inflate(R.layout.rename_file_dialog, null, false);

        // Текстовое поле для ввода нового имени файла
        final EditText newFileName = v.findViewById(R.id.newFileName);

        // Получение переданных аргументов с ключами name и position и присвоение локальным переменным
        Bundle args = getArguments();
        name = args.getString("name");
        position = args.getInt("position");

        // Установить в качестве введенного текста старое название файла
        newFileName.setText(name);

        // Проверить на существование точки, которая пишется перед расширением файла String.lastInfdexOf(char) - возвращает последнюю позицию в виде числового значения указанного внутри скобок символа или строки
        if (name.lastIndexOf('.') > 0) {
            // Если есть расширение файла присвоить это расширение переменной extension путём получения только расширения через функцию String.substring(startIndex, endIndex)
            extension = name.substring(name.lastIndexOf('.'), name.length());
        }

        // При клике на текстовое поле открывается клавиатура и в это время надо выбрать из старого названия часть строки до расширения файла, для удобства работы пользователя для исключения траты времени на выбор текста в текстовом поле и удаление ненужной части
        newFileName.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // При клике текстовое поле передаётся в качестве объекта класса View тут необходимо используя приведение типов из типа View привести к типу EditText
                EditText editText = (EditText) v;
                // Метод setSelection(start, stop) принимает два параметра: начальный индекс символа от куда начать выделение и конечную позицию символа до какого символа выделить тут выбирается с первого символа (это индекс 0) и до позиции последней точки, которая идёт перед расширением файла, точка не выделяется
                newFileName.setSelection(0, editText.getText().toString().lastIndexOf('.'));
            }
        });

        // Установить полученный через LayoutInflater макет диаологовому окну
        builder.setView(v);
        // Установить сообщение диаологового окна для удобства объяснения пользователю назначения диалогового окна
        builder.setMessage("Введите новое название файла");
        // Установить кнопку Сохранить и соответствующее действие выполняемое при клике на кнопку сохранить
        builder.setPositiveButton("Сохранить", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                // Получить и присвоить переменной введенный в текстовое поле текст
                String tempName = newFileName.getText().toString();
                // Если длина введенного названия больше 0
                if (tempName.length() > 0) {
                    // Здесь идет проверка не удалено ли расширение файла и если функция lastIndexOf возвращает значение меньше 0, значит расширение в текстовом поле удалено
                    if (tempName.lastIndexOf('.') <= 0) {
                        // в таком случае расширение которое было получено в верхней части этого кода добавить к концу нового названия файла
                        tempName += extension;
                    }
                    // Вызвать функцию renameFile через ссылку на экземпляр адаптера
                    adapter.renameFile(tempName, position);
                    // Закрыть диалоговое окно
                    dismiss();
                }
            }
        });

        // Установить кнопку Отмена, которая ничего не делает и только закрывает диалоговое окно
        builder.setNegativeButton("Отмена", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dismiss();
            }
        });

        // Вернуть экземпляр диалогового окна
        return builder.create();
    }
}

Откройте файл FullScreenViewActivity.java и тут вы можете видеть много сгенерированного кода измените код файла для получения следующего кода:

public class FullScreenViewActivity extends AppCompatActivity {

    private static final boolean AUTO_HIDE = true;
    private static final int AUTO_HIDE_DELAY_MILLIS = 3000;

    private static final int UI_ANIMATION_DELAY = 300;
    private final Handler mHideHandler = new Handler();
    private View mContentView;
    private final Runnable mHidePart2Runnable = new Runnable() {
        @SuppressLint("InlinedApi")
        @Override
        public void run() {
            mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE
                    | View.SYSTEM_UI_FLAG_FULLSCREEN
                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                    | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
                    | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                    | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
        }
    };
    private View mControlsView;
    private final Runnable mShowPart2Runnable = new Runnable() {
        @Override
        public void run() {
            // Delayed display of UI elements
            ActionBar actionBar = getSupportActionBar();
            if (actionBar != null) {
                actionBar.show();
            }
            mControlsView.setVisibility(View.VISIBLE);
        }
    };
    private boolean mVisible;
    private final Runnable mHideRunnable = new Runnable() {
        @Override
        public void run() {
            hide();
        }
    };
    /**
     * Touch listener to use for in-layout UI controls to delay hiding the
     * system UI. This is to prevent the jarring behavior of controls going away
     * while interacting with activity UI.
     */
    private final View.OnTouchListener mDelayHideTouchListener = new View.OnTouchListener() {
        @Override
        public boolean onTouch(View view, MotionEvent motionEvent) {
            if (AUTO_HIDE) {
                delayedHide(AUTO_HIDE_DELAY_MILLIS);
            }
            return false;
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_full_screen_view);

        // Задача теперь получить два переданных параметра, один из них тип данных для показа (0 - фото, 1 - видео), второй путь к папке откуда брать информацию
        int type = getIntent().getIntExtra("type", 0);
        String path = getIntent().getStringExtra("path");

        ImageView imageContent = findViewById(R.id.imageContent);
        VideoView videoContent = findViewById(R.id.videoContent);

        // Надо показать фото
        if (type == 0)
        {
            // Делаем ImageView видимым
            imageContent.setVisibility(View.VISIBLE);
            // Устанавливаем изображение для ImageView
            imageContent.setImageDrawable(Drawable.createFromPath(path));
        }

        // Если надо показать видео
        if(type == 1)
        {
            // Сделать VideoView видимым
            videoContent.setVisibility(View.VISIBLE);
            // Указать путь к видеофайлу
            videoContent.setVideoPath(path);
            // Запустить проигрывание видео
            videoContent.start();
        }

        mVisible = true;
        mContentView = findViewById(R.id.fullscreen_content);


        // Set up the user interaction to manually show or hide the system UI.
        mContentView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                toggle();
            }
        });
    }

    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);

        // Trigger the initial hide() shortly after the activity has been
        // created, to briefly hint to the user that UI controls
        // are available.
        delayedHide(100);
    }

    private void toggle() {
        if (mVisible) {
            hide();
        } else {
            show();
        }
    }

    private void hide() {
        // Hide UI first
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.hide();
        }
//        mControlsView.setVisibility(View.GONE);
        mVisible = false;

        // Schedule a runnable to remove the status and navigation bar after a delay
        mHideHandler.removeCallbacks(mShowPart2Runnable);
        mHideHandler.postDelayed(mHidePart2Runnable, UI_ANIMATION_DELAY);
    }

    @SuppressLint("InlinedApi")
    private void show() {
        // Show the system bar
        mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
        mVisible = true;

        // Schedule a runnable to display UI elements after a delay
        mHideHandler.removeCallbacks(mHidePart2Runnable);
        mHideHandler.postDelayed(mShowPart2Runnable, UI_ANIMATION_DELAY);
    }

    /**
     * Schedules a call to hide() in delay milliseconds, canceling any
     * previously scheduled calls.
     */
    private void delayedHide(int delayMillis) {
        mHideHandler.removeCallbacks(mHideRunnable);
        mHideHandler.postDelayed(mHideRunnable, delayMillis);
    }
}
Большая часть работы сделана, теперь переходим к заключительной части в следующем уроке.