From 554d55d9a8fc7ad900052218481d9aa01068ed8c Mon Sep 17 00:00:00 2001 From: Mikhail M Date: Mon, 20 Feb 2023 20:56:11 +0300 Subject: [PATCH 01/13] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B1=D0=B0=D0=B3=20=D1=81=20=D0=BE=D0=B1=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=D0=BC=20=D0=BE=D1=82?= =?UTF-8?q?=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/mmu/tinkoffkinolab/MainActivity.java | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java index 340ac80..35429fb 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java @@ -51,6 +51,7 @@ import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.Stream; import io.swagger.client.ApiException; import io.swagger.client.api.FilmsApi; @@ -346,6 +347,7 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) } case R.id.action_refresh: { + Objects.requireNonNull(txtQuery.getText()).clear(); refreshUIContent(); break; } @@ -406,7 +408,7 @@ private View.OnClickListener getOnLikeButtonClickListener(Map ca private void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { boolean isBottomReached = cardsContainer.getBottom() - v.getBottom() - scrollY == 0; - if (isBottomReached) + if (isBottomReached && !_isFiltered) { if (_currentViewMode == ViewMode.POPULAR && _currentPageNumber < _topFilmsPagesCount) { @@ -453,7 +455,9 @@ public void afterTextChanged(Editable s) } else { - filterFilmCardsUI(s.toString()); + filterFilmCardsUI(s.toString(), _currentViewMode == ViewMode.FAVOURITES ? + getFavouritesMap().values().stream() : _currentViewMode == ViewMode.POPULAR ? + _cardList.stream() : Stream.empty()); } } }; @@ -493,7 +497,7 @@ private void _initEventHandlers() /** * Метод показа (вывода из невидимости) всех карточек фильмов * - * @apiNote Вызов имеет смысл, только если перед этим был вызов {@link #filterFilmCardsUI(String)} + * @apiNote Вызов имеет смысл, только если перед этим был вызов {@link #filterFilmCardsUI(String, Stream)} )} * @implNote Сбрасывает признак фильтрации списка {@link #_isFiltered} */ private void showFilmCardsUI() @@ -515,17 +519,17 @@ private void showFilmCardsUI() * @apiNote Метод не выдаёт совпадения для слов короче 3-х символов (для исключения предлогов) * @implSpec При пустом query не делает НИЧЕГО - для отмены фильтра используйте {@link #showFilmCardsUI()} */ - private void filterFilmCardsUI(String query) + private void filterFilmCardsUI(String query, Stream> cardStream) { if (query.isBlank()) { return; } - final var foundFilms = _cardList.stream().filter(x -> { - final var words = Arrays.stream(Objects.requireNonNull(x.get(Constants.ADAPTER_TITLE)).split(" ")); - return words.anyMatch(t -> t.length() > 2 && t.toLowerCase().startsWith(query.strip().toLowerCase())); - }) - .collect(Collectors.toList()); + final var foundFilms = cardStream.filter(x -> { + final var words = Arrays.stream(Objects.requireNonNull(x.get(Constants.ADAPTER_TITLE)).split(" ")); + return words.anyMatch(t -> t.length() > 2 && t.toLowerCase().startsWith(query.strip().toLowerCase())); + }) + .collect(Collectors.toList()); for (int i = 0; i < cardsContainer.getChildCount(); i++) { @@ -556,6 +560,7 @@ private void filterFilmCardsUI(String query) */ private void refreshUIContent() { + Objects.requireNonNull(txtQuery.getText()).clear(); if (this._currentViewMode == ViewMode.POPULAR) { switchUIToPopularFilmsAsync(true, true); @@ -740,8 +745,16 @@ else if (!isDownloadNew) else { fillCardListUIFrom(0, _cardList); - // на основе кода отсюда: https://stackoverflow.com/a/3263540/2323972 - scroller.post(() -> scroller.scrollTo(0, _lastListViewPos)); + final var query = Objects.requireNonNull(txtQuery.getText()).toString(); + if (query.isBlank()) + { + // на основе кода отсюда: https://stackoverflow.com/a/3263540/2323972 + scroller.post(() -> scroller.scrollTo(0, _lastListViewPos)); + } + else + { + filterFilmCardsUI(query, _cardList.stream()); + } } } @@ -768,7 +781,12 @@ private void switchUIToFavouriteFilms(boolean forceRefresh) if (!getFavouritesMap().isEmpty()) { fillCardListUIFrom(0, new ArrayList<>(getFavouritesMap().values())); - if (!forceRefresh) + final var query = Objects.requireNonNull(txtQuery.getText()).toString(); + if (!query.isBlank()) + { + filterFilmCardsUI(query, getFavouritesMap().values().stream()); + } + else if (!forceRefresh) { scroller.post(() -> scroller.scrollTo(0, _lastListViewPos2)); } From f73400ac65bcf54de497e746fe3ec1cabaee4697 Mon Sep 17 00:00:00 2001 From: Mikhail M Date: Tue, 21 Feb 2023 16:57:32 +0300 Subject: [PATCH 02/13] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BF=D0=BE=D0=B2=D1=82=D0=BE=D1=80=D0=BD=D1=83=D1=8E?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D1=83=20=D0=BA?= =?UTF-8?q?=D0=B5=D1=88=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D0=BF=D0=BE=D1=81=D1=82=D0=B5=D1=80=D0=B0=20?= =?UTF-8?q?=D1=84=D0=B8=D0=BB=D1=8C=D0=BC=D0=B0=20=D0=BD=D0=B0=20=D1=81?= =?UTF-8?q?=D0=BB=D1=83=D1=87=D0=B0=D0=B9,=20=D0=B5=D1=81=D0=BB=D0=B8=20?= =?UTF-8?q?=D1=84=D0=B0=D0=B9=D0=BB=20=D0=BD=D0=B5=20=D0=BD=D0=B0=D0=B9?= =?UTF-8?q?=D0=B4=D0=B5=D0=BD=20=D0=BD=D0=B0=20=D0=B4=D0=B8=D1=81=D0=BA?= =?UTF-8?q?=D0=B5.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/mmu/tinkoffkinolab/CardActivity.java | 13 ++- .../org/mmu/tinkoffkinolab/Constants.java | 11 ++- .../org/mmu/tinkoffkinolab/MainActivity.java | 96 +++++++++++++++---- 3 files changed, 91 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java b/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java index 1e0c53c..28ec544 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java @@ -44,9 +44,10 @@ public class CardActivity extends AppCompatActivity //region 'Типы' - private class WebDataDownloadTask extends AsyncTask + private class WebDataDownloadTask extends AsyncTask { private final FilmsApi filmsApi; + private final View progBar = findViewById(R.id.progress_bar); private Map.Entry error; public Map.Entry getError() @@ -63,26 +64,25 @@ public WebDataDownloadTask(FilmsApi engine) protected void onPreExecute() { super.onPreExecute(); - var progBar = findViewById(R.id.progress_bar); progBar.setVisibility(View.VISIBLE); Log.d(Constants.LOG_TAG, "Начало загрузки веб-ресурса..."); } @Override - protected Void doInBackground(String... request) + protected Void doInBackground(Void... unused) { try { final var response = filmsApi.apiV22FilmsIdGet(Integer.parseInt(filmId)); _cardData.put(Constants.ADAPTER_TITLE, response.getNameRu()); - final var geners = "\n\n Жанры: " + response.getGenres().stream() + final var genres = "\n\n Жанры: " + response.getGenres().stream() .map(g -> g.getGenre() + ", ") .collect(Collectors.joining()) .replaceFirst(",\\s*$", ""); final var countries = "\n\n Страны: " + response.getCountries().stream() .map(country -> country.getCountry() + ", ") .collect(Collectors.joining()).replaceFirst(",\\s*$", ""); - final var res = response.getDescription() + geners + countries; + final var res = response.getDescription() + genres + countries; _cardData.put(Constants.ADAPTER_CONTENT, res); _cardData.put(Constants.ADAPTER_POSTER_PREVIEW_URL, response.getPosterUrl()); } @@ -114,7 +114,6 @@ protected Void doInBackground(String... request) protected void onPostExecute(Void unused) { super.onPostExecute(unused); - var progBar = findViewById(R.id.progress_bar); progBar.setVisibility(View.GONE); if (downloadTask.getError() != null) { @@ -188,7 +187,7 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) */ private void fillCardUI() { - // параметры fit() и centerCrop() сильно замедляют загрузку. + // N.B.: параметры fit() и centerCrop() могут сильно замедлять загрузку! Picasso.get().load(_cardData.get(Constants.ADAPTER_POSTER_PREVIEW_URL)).fit().centerCrop().into(imgPoster); txtHeader.setText(_cardData.get(Constants.ADAPTER_TITLE)); Objects.requireNonNull(getSupportActionBar()).setDisplayShowTitleEnabled(false); diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/Constants.java b/app/src/main/java/org/mmu/tinkoffkinolab/Constants.java index 18695c2..f98d814 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/Constants.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/Constants.java @@ -1,6 +1,8 @@ package org.mmu.tinkoffkinolab; +import jp.wasabeef.picasso.transformations.RoundedCornersTransformation; + public class Constants { public static final String ADAPTER_FILM_ID = "ID"; @@ -13,6 +15,7 @@ public class Constants public static final String KEY_VALUE_SEPARATOR = "="; public static final String FILM_START_TOKEN = "{"; public static final String FILM_END_TOKEN = "}"; + public static final RoundedCornersTransformation ROUNDED_CORNERS_TRANSFORMATION = new RoundedCornersTransformation(30, 10); /** * Демо-ключ неофициального API Кинопоиска * @@ -24,10 +27,10 @@ public class Constants * @implSpec В качестве альтернативы вы можете зарегистрироваться самостоятельно и получить * собственный ключ, но тогда будет действовать ограничение в 500 запросов в день. */ - static final String KINO_DEMO_API_KEY = "e30ffed0-76ab-4dd6-b41f-4c9da2b2735b"; - static final String ADAPTER_POSTER_PREVIEW_URL = "ImageUrl"; - static final String UNKNOWN_WEB_ERROR_MES = "Ошибка загрузки данных по сети:"; - static final String KINO_API_ERROR_MES = "Ошибка API KinoPoisk"; + public static final String KINO_DEMO_API_KEY = "e30ffed0-76ab-4dd6-b41f-4c9da2b2735b"; + public static final String ADAPTER_POSTER_PREVIEW_URL = "ImageUrl"; + public static final String UNKNOWN_WEB_ERROR_MES = "Ошибка загрузки данных по сети:"; + public static final String KINO_API_ERROR_MES = "Ошибка API KinoPoisk"; enum TopFilmsType { diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java index 35429fb..2da5f37 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java @@ -8,6 +8,7 @@ import androidx.cardview.widget.CardView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import android.annotation.SuppressLint; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; @@ -32,6 +33,7 @@ import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.snackbar.Snackbar; import com.google.android.material.textfield.TextInputEditText; +import com.squareup.picasso.Callback; import com.squareup.picasso.Picasso; import java.io.BufferedReader; @@ -55,12 +57,10 @@ import io.swagger.client.ApiException; import io.swagger.client.api.FilmsApi; -import jp.wasabeef.picasso.transformations.RoundedCornersTransformation; public class MainActivity extends AppCompatActivity { - //region 'Типы' private enum ViewMode @@ -169,7 +169,6 @@ protected void onPostExecute(Integer pagesCount) //region 'Поля и константы' private static final List> _cardList = new ArrayList<>(); - public TextWatcher _textWatcher; public File _favouritesListFilePath; private File _imageCacheDirPath; @@ -425,15 +424,17 @@ else if (scrollY == 0) } } + + private TextWatcher searchTextChangeWatcher; /** * Обработчик изменения текста в виджете Поиска */ @NonNull private TextWatcher getSearchTextChangeWatcher() { - if (_textWatcher == null) + if (this.searchTextChangeWatcher == null) { - _textWatcher = new TextWatcher() + searchTextChangeWatcher = new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) @@ -462,7 +463,19 @@ public void afterTextChanged(Editable s) } }; } - return _textWatcher; + return searchTextChangeWatcher; + } + + /** + * Обработчик клика на элемент списка (карточку Фильма) - открывает окно с подробным описанием Фильма. + * + * @param id ИД Фильма в API Kinopoisk + * @param title Название (будет отображаться в заголовке карточки до тех пор, пока не будет загружено подробное описание) + */ + @NonNull + private View.OnClickListener getOnListItemClickListener(String id, String title) + { + return (View v) -> showFilmCardActivity(id, title); } //endregion 'Обработчики' @@ -569,6 +582,7 @@ else if (this._currentViewMode == ViewMode.FAVOURITES) { cardsContainer.setVisibility(View.INVISIBLE); switchUIToFavouriteFilms(true); + // добавляем задержку, чтобы юзер мог четко определить замену списка на новый cardsContainer.postDelayed(() -> { swipeRefreshContainer.setRefreshing(false); cardsContainer.setVisibility(View.VISIBLE); @@ -585,6 +599,7 @@ private void fillCardListUIFrom(int startItemIndex, List> ca { for (int i = startItemIndex; i < cardList.size(); i++) { + @SuppressLint("InflateParams") final var listItem = _layoutInflater.inflate(R.layout.list_item, null); final var cardData = cardList.get(i); final var id = cardData.get(Constants.ADAPTER_FILM_ID); @@ -594,31 +609,39 @@ private void fillCardListUIFrom(int startItemIndex, List> ca ((TextView) listItem.findViewById(R.id.card_content)).setText(cardData.get(ADAPTER_CONTENT)); final var imgView = ((ImageView) listItem.findViewById(R.id.poster_preview)); final var imageUrl = cardData.get(Constants.ADAPTER_POSTER_PREVIEW_URL); - final var cachedImageFilePath = cardData.getOrDefault(ADAPTER_IMAGE_PREVIEW_FILE_PATH, ""); - //noinspection ConstantConditions - if (cachedImageFilePath.isEmpty()) + final var cachedImageFilePath = cardData.get(ADAPTER_IMAGE_PREVIEW_FILE_PATH); + File previewImageFile = null; + if (cachedImageFilePath != null && !cachedImageFilePath.isBlank()) + { + previewImageFile = new File(cachedImageFilePath); + } + if (previewImageFile == null) { - Picasso.get().load(imageUrl).transform(new RoundedCornersTransformation(30, 10)).into(imgView); + Picasso.get().load(imageUrl).transform(ROUNDED_CORNERS_TRANSFORMATION).into(imgView); } else { - final var previewImageFile = new File(cachedImageFilePath); if (previewImageFile.exists()) { imgView.setImageURI(Uri.fromFile(previewImageFile)); - //imgView.setImageDrawable(RoundedBitmapDrawable.createFromPath(cachedImageFilePath)); Log.d(LOG_TAG, "Картинка загружена из файла: " + cachedImageFilePath); } + else + { + restoreImageCacheFromURL(imgView, imageUrl, cachedImageFilePath); + Log.d(LOG_TAG, "Промах кэша - видимо файл был удалён - картинка будет загружена из Сети:\n " + + cachedImageFilePath); + } } cardsContainer.addView(listItem); - listItem.setOnClickListener(v -> showFilmCardActivity(id, title)); + listItem.setOnClickListener(this.getOnListItemClickListener(id, title)); final ImageView imgViewLike = listItem.findViewById(R.id.like_image_view); // TODO: возможно фильм нужно искать по названию, а не по ИД, на случай, если ИД поменяется? if (getFavouritesMap().containsKey(id)) { imgViewLike.setImageResource(R.drawable.baseline_favorite_24); } - final var likeButtonClickHandler = getOnLikeButtonClickListener( + final var likeButtonClickHandler = this.getOnLikeButtonClickListener( cardData, id, imgView, imgViewLike); imgViewLike.setOnClickListener(likeButtonClickHandler); // Требование ТЗ: "При длительном клике на карточку, фильм помещается в избранное" @@ -629,6 +652,40 @@ private void fillCardListUIFrom(int startItemIndex, List> ca } } + private void restoreImageCacheFromURL(ImageView imgView, String imageUrl, String cachedImageFilePath) + { + Picasso.get().load(imageUrl).transform(ROUNDED_CORNERS_TRANSFORMATION) + .into(imgView, new Callback() + { + @Override + public void onSuccess() + { + // TODO: без задержки, картинки не успевают прорисоваться, но слишком большая задержка, + // тоже опасна - юзер уже мог переключить представление (хотя объект не должен уничтожаться). + imgView.postDelayed(() -> extractImageToDiskCache(imgView, cachedImageFilePath), 300); + } + + @Override + public void onError(Exception e) + { + Log.e(LOG_TAG, "Ошибка загрузки мини постера фильма по адресу:\n " + imageUrl, e); + } + }); + } + + private void extractImageToDiskCache(ImageView imgViewSource, String cachedImageFilePath) + { + try (var outStream = new FileOutputStream(cachedImageFilePath)) + { + Utils.convertDrawableToBitmap(imgViewSource.getDrawable()).compress( + Bitmap.CompressFormat.WEBP, 80, outStream); + } + catch (IOException e) + { + Log.e(LOG_TAG, "Ошибка записи в файл:\n " + cachedImageFilePath, e); + } + } + /** * Метод добавления фильма в список Избранного (или удаления из него) * @@ -818,10 +875,13 @@ else if (_currentViewMode == ViewMode.POPULAR) @NonNull private void showFilmCardActivity(String kinoApiFilmId, String cardTitle) { - var switchActivityIntent = new Intent(getApplicationContext(), CardActivity.class); - switchActivityIntent.putExtra(Constants.ADAPTER_TITLE, cardTitle); - switchActivityIntent.putExtra(Constants.ADAPTER_FILM_ID, kinoApiFilmId); - startActivity(switchActivityIntent); + final var switchToCardActivityIntent = new Intent(getApplicationContext(), CardActivity.class); + switchToCardActivityIntent.putExtra(Constants.ADAPTER_TITLE, cardTitle); + switchToCardActivityIntent.putExtra(Constants.ADAPTER_FILM_ID, kinoApiFilmId); + final var extraData = new Bundle(2); + extraData.clear(); + extraData.putString(Constants.ADAPTER_TITLE, cardTitle); + startActivity(switchToCardActivityIntent); } /** From 113cd00711a3630d372e86be2bc54b5d07b77769 Mon Sep 17 00:00:00 2001 From: Mikhail M Date: Wed, 22 Feb 2023 13:18:02 +0300 Subject: [PATCH 03/13] =?UTF-8?q?CardActivity:=20=D0=9F=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20=D0=BD=D0=B0=20=D0=BE=D1=82?= =?UTF-8?q?=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=A4?= =?UTF-8?q?=D0=B8=D0=BB=D1=8C=D0=BC=D0=B0=20=D0=B2=20=D0=BE=D1=82=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE=D0=BC=20=D1=84=D1=80=D0=B0=D0=B3?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D1=82=D0=B5.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/mmu/tinkoffkinolab/CardActivity.java | 28 +++++--- .../org/mmu/tinkoffkinolab/CardFragment.java | 70 +++++++++++++++++++ .../org/mmu/tinkoffkinolab/MainActivity.java | 69 ++++++------------ .../java/org/mmu/tinkoffkinolab/Utils.java | 17 +++++ app/src/main/res/layout/activity_card.xml | 55 +++------------ app/src/main/res/layout/fragment_card.xml | 59 ++++++++++++++++ app/src/main/res/values/strings.xml | 2 + 7 files changed, 196 insertions(+), 104 deletions(-) create mode 100644 app/src/main/java/org/mmu/tinkoffkinolab/CardFragment.java create mode 100644 app/src/main/res/layout/fragment_card.xml diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java b/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java index 28ec544..5618baa 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java @@ -1,7 +1,9 @@ package org.mmu.tinkoffkinolab; +import android.content.Context; import android.os.AsyncTask; import android.os.Bundle; +import android.util.AttributeSet; import android.util.Log; import android.view.MenuItem; import android.view.View; @@ -9,7 +11,10 @@ import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.snackbar.Snackbar; @@ -146,16 +151,23 @@ protected void onCreate(Bundle savedInstanceState) this.setSupportActionBar(customToolBar); Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayShowHomeEnabled(true); - - imgPoster = findViewById(R.id.poster_image_view); - txtHeader = findViewById(R.id.card_title); - txtContent = findViewById(R.id.card_content); - androidContentView = findViewById(android.R.id.content); - filmId = getIntent().getStringExtra(Constants.ADAPTER_FILM_ID); - getFilmDataAsync(); + + getSupportFragmentManager().registerFragmentLifecycleCallbacks(new FragmentManager.FragmentLifecycleCallbacks() + { + @Override + public void onFragmentViewCreated(@NonNull FragmentManager fm, @NonNull Fragment f, @NonNull View v, @Nullable Bundle savedInstanceState) + { + imgPoster = v.findViewById(R.id.poster_image_view); + txtHeader = v.findViewById(R.id.card_title); + txtContent = v.findViewById(R.id.card_content); + androidContentView = v.findViewById(android.R.id.content); + super.onFragmentViewCreated(fm, f, v, savedInstanceState); + getFilmDataAsync(); + } + }, false); } - + /** * Обработчик нажатия кнопки меню Назад * diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/CardFragment.java b/app/src/main/java/org/mmu/tinkoffkinolab/CardFragment.java new file mode 100644 index 0000000..9ee4d4d --- /dev/null +++ b/app/src/main/java/org/mmu/tinkoffkinolab/CardFragment.java @@ -0,0 +1,70 @@ +package org.mmu.tinkoffkinolab; + +import android.os.Bundle; + +import androidx.fragment.app.Fragment; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +/** + * A simple {@link Fragment} subclass. + * Use the {@link CardFragment#newInstance} factory method to + * create an instance of this fragment. + */ +public class CardFragment extends Fragment +{ + + // TODO: Rename parameter arguments, choose names that match + // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER + private static final String ARG_PARAM1 = "param1"; + private static final String ARG_PARAM2 = "param2"; + + // TODO: Rename and change types of parameters + private String mParam1; + private String mParam2; + + // Required empty public constructor + public CardFragment() + { + } + + /** + * Use this factory method to create a new instance of + * this fragment using the provided parameters. + * + * @param param1 Parameter 1. + * @param param2 Parameter 2. + * @return A new instance of fragment CardFragment. + */ + // TODO: Rename and change types and number of parameters + public static CardFragment newInstance(String param1, String param2) + { + CardFragment fragment = new CardFragment(); + Bundle args = new Bundle(); + args.putString(ARG_PARAM1, param1); + args.putString(ARG_PARAM2, param2); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + if (getArguments() != null) + { + mParam1 = getArguments().getString(ARG_PARAM1); + mParam2 = getArguments().getString(ARG_PARAM2); + } + } + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) + { + // TODO Auto-generated method stub + return inflater.inflate(R.layout.fragment_card, container, false); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java index 2da5f37..0062f95 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java @@ -1,6 +1,7 @@ package org.mmu.tinkoffkinolab; import static org.mmu.tinkoffkinolab.Constants.*; +import static org.mmu.tinkoffkinolab.Utils.*; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; @@ -36,22 +37,8 @@ import com.squareup.picasso.Callback; import com.squareup.picasso.Picasso; -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileOutputStream; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.util.AbstractMap; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; +import java.io.*; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -166,7 +153,6 @@ protected void onPostExecute(Integer pagesCount) - //region 'Поля и константы' private static final List> _cardList = new ArrayList<>(); public File _favouritesListFilePath; @@ -205,7 +191,6 @@ protected void onPostExecute(Integer pagesCount) - //region 'Свойства' private Map> favouritesMap; @@ -214,7 +199,7 @@ protected void onPostExecute(Integer pagesCount) * Возвращает список избранных фильмов * * @implSpec ВНИМАНИЕ: ИД фильма должен идти первой строчкой, иначе метод загрузки из файла не будет - * работать корректно ({@link #loadFavouritesList()}) + * работать корректно ({@link #loadFavouritesList()}) */ public Map> getFavouritesMap() { @@ -247,7 +232,6 @@ public AlertDialog getConfirmClearDialog() - //region 'Обработчики' /** @@ -273,7 +257,7 @@ protected void onCreate(Bundle savedInstanceState) if (Debug.isDebuggerConnected()) { Picasso.get().setIndicatorsEnabled(true); - Picasso.get().setLoggingEnabled(true); + //Picasso.get().setLoggingEnabled(true); } isRus = Locale.getDefault().getLanguage().equalsIgnoreCase("ru"); @@ -426,6 +410,7 @@ else if (scrollY == 0) private TextWatcher searchTextChangeWatcher; + /** * Обработчик изменения текста в виджете Поиска */ @@ -440,12 +425,12 @@ private TextWatcher getSearchTextChangeWatcher() public void beforeTextChanged(CharSequence s, int start, int count, int after) { } - + @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } - + @Override public void afterTextChanged(Editable s) { @@ -482,7 +467,6 @@ private View.OnClickListener getOnListItemClickListener(String id, String title) - //region 'Методы' /** @@ -539,10 +523,10 @@ private void filterFilmCardsUI(String query, Stream> cardStr return; } final var foundFilms = cardStream.filter(x -> { - final var words = Arrays.stream(Objects.requireNonNull(x.get(Constants.ADAPTER_TITLE)).split(" ")); - return words.anyMatch(t -> t.length() > 2 && t.toLowerCase().startsWith(query.strip().toLowerCase())); - }) - .collect(Collectors.toList()); + final var words = Arrays.stream(Objects.requireNonNull(x.get(Constants.ADAPTER_TITLE)).split(" ")); + return words.anyMatch(t -> t.length() > 2 && t.toLowerCase().startsWith(query.strip().toLowerCase())); + }) + .collect(Collectors.toList()); for (int i = 0; i < cardsContainer.getChildCount(); i++) { @@ -599,8 +583,7 @@ private void fillCardListUIFrom(int startItemIndex, List> ca { for (int i = startItemIndex; i < cardList.size(); i++) { - @SuppressLint("InflateParams") - final var listItem = _layoutInflater.inflate(R.layout.list_item, null); + @SuppressLint("InflateParams") final var listItem = _layoutInflater.inflate(R.layout.list_item, null); final var cardData = cardList.get(i); final var id = cardData.get(Constants.ADAPTER_FILM_ID); ((TextView) listItem.findViewById(R.id.film_id_holder)).setText(id); @@ -660,11 +643,11 @@ private void restoreImageCacheFromURL(ImageView imgView, String imageUrl, String @Override public void onSuccess() { - // TODO: без задержки, картинки не успевают прорисоваться, но слишком большая задержка, + //TODO: без задержки, картинки не успевают прорисоваться, но слишком большая задержка, // тоже опасна - юзер уже мог переключить представление (хотя объект не должен уничтожаться). imgView.postDelayed(() -> extractImageToDiskCache(imgView, cachedImageFilePath), 300); } - + @Override public void onError(Exception e) { @@ -673,19 +656,6 @@ public void onError(Exception e) }); } - private void extractImageToDiskCache(ImageView imgViewSource, String cachedImageFilePath) - { - try (var outStream = new FileOutputStream(cachedImageFilePath)) - { - Utils.convertDrawableToBitmap(imgViewSource.getDrawable()).compress( - Bitmap.CompressFormat.WEBP, 80, outStream); - } - catch (IOException e) - { - Log.e(LOG_TAG, "Ошибка записи в файл:\n " + cachedImageFilePath, e); - } - } - /** * Метод добавления фильма в список Избранного (или удаления из него) * @@ -780,7 +750,7 @@ private void switchUIToPopularFilmsAsync(boolean ifBeginFromPageOne, boolean isD } _currentViewMode = ViewMode.POPULAR; customToolBar.setTitle(R.string.action_popular_title); - + _lastListViewPos2 = scroller.getScrollY(); if (ifBeginFromPageOne && isDownloadNew) @@ -819,6 +789,7 @@ private void switchUIToFavouriteFilms() { switchUIToFavouriteFilms(false); } + /** * Метода показа Избранных фильмов */ @@ -908,8 +879,8 @@ else if (!line.equalsIgnoreCase(FILM_START_TOKEN)) } catch (IOException | RuntimeException e) { - Log.e(LOG_TAG,"Ошибка чтения файла: " + FAVOURITES_LIST_FILE_NAME, e); - Toast.makeText(this,"Не удалось прочитать файл Избранного!", Toast.LENGTH_SHORT).show(); + Log.e(LOG_TAG, "Ошибка чтения файла: " + FAVOURITES_LIST_FILE_NAME, e); + Toast.makeText(this, "Не удалось прочитать файл Избранного!", Toast.LENGTH_SHORT).show(); } } @@ -943,7 +914,7 @@ private void saveFavouritesList() } catch (IOException | RuntimeException e) { - Log.e(LOG_TAG,"Ошибка записи файла: " + _favouritesListFilePath.toString(), e); + Log.e(LOG_TAG, "Ошибка записи файла: " + _favouritesListFilePath.toString(), e); } } diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/Utils.java b/app/src/main/java/org/mmu/tinkoffkinolab/Utils.java index 407d375..bfb6dcb 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/Utils.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/Utils.java @@ -1,5 +1,7 @@ package org.mmu.tinkoffkinolab; +import static org.mmu.tinkoffkinolab.Constants.LOG_TAG; + import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; @@ -8,6 +10,8 @@ import android.widget.ImageView; import java.io.BufferedInputStream; +import java.io.FileOutputStream; +import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLConnection; @@ -53,4 +57,17 @@ public static Bitmap convertDrawableToBitmap(Drawable pd) pd.draw(canvas); return bm; } + + public static void extractImageToDiskCache(ImageView imgViewSource, String cachedImageFilePath) + { + try (var outStream = new FileOutputStream(cachedImageFilePath)) + { + Utils.convertDrawableToBitmap(imgViewSource.getDrawable()).compress( + Bitmap.CompressFormat.WEBP, 80, outStream); + } + catch (IOException e) + { + Log.e(LOG_TAG, "Ошибка записи в файл:\n " + cachedImageFilePath, e); + } + } } diff --git a/app/src/main/res/layout/activity_card.xml b/app/src/main/res/layout/activity_card.xml index 05a43c8..7c6df63 100644 --- a/app/src/main/res/layout/activity_card.xml +++ b/app/src/main/res/layout/activity_card.xml @@ -1,4 +1,5 @@ + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a7216bc..29b027f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -10,4 +10,6 @@ Clear All Are you sure want to delete all items? Confirmation + + Hello blank fragment \ No newline at end of file From ea7f3afe77da64548a49a83db91250167aaafcc1 Mon Sep 17 00:00:00 2001 From: Mikhail M Date: Wed, 22 Feb 2023 17:52:49 +0300 Subject: [PATCH 04/13] =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=80=D0=B5=D0=B0=D0=BA=D1=86=D0=B8=D1=8E=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=BE=D0=B2=D0=BE=D1=80=D0=BE=D1=82=20=D1=8D?= =?UTF-8?q?=D0=BA=D1=80=D0=B0=D0=BD=D0=B0=20-=20=D0=B1=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D1=88=D0=B5=20=D0=BD=D0=B5=20=D0=BF=D1=80=D0=BE=D0=B8=D1=81?= =?UTF-8?q?=D1=85=D0=BE=D0=B4=D0=B8=D1=82=20=D0=BF=D0=BE=D0=B2=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=BD=D0=B0=D1=8F=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B8?= =?UTF-8?q?=D0=B7=20=D0=A1=D0=B5=D1=82=D0=B8,=20=D0=BE=D0=B4=D0=BD=D0=B0?= =?UTF-8?q?=D0=BA=D0=BE=20=D0=BF=D1=80=D0=B8=D1=88=D0=BB=D0=BE=D1=81=D1=8C?= =?UTF-8?q?=20=D0=B2=D0=BD=D0=B5=D1=81=D1=82=D0=B8=20=D0=BA=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D1=8B=D0=BB=D1=8C=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D1=80=D0=B8=D1=81=D0=BE=D0=B2=D0=BA=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=D0=B5=D0=BB=D0=B8=20=D0=B8=D0=BD=D1=81=D1=82=D1=80?= =?UTF-8?q?=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=BE=D0=B2=20=D0=BF=D0=BE=D1=81?= =?UTF-8?q?=D0=BB=D0=B5=20=D0=BF=D0=BE=D0=B2=D0=BE=D1=80=D0=BE=D1=82=D0=B0?= =?UTF-8?q?.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B3=D0=BE=D1=82=D0=BE=D0=B2=D0=BA=D1=83=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D0=BE=D1=82=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE=D0=B3=D0=BE?= =?UTF-8?q?=20=D1=84=D1=80=D0=B0=D0=B3=D0=BC=D0=B5=D0=BD=D1=82=D0=B0,=20?= =?UTF-8?q?=D0=BD=D0=BE=20=D0=BE=D0=BD=20=D0=BF=D0=BE=D0=BA=D0=B0=20=D0=BD?= =?UTF-8?q?=D0=B5=20=D0=B2=D0=B8=D0=B4=D0=B5=D0=BD=20=D0=BF=D1=80=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B2=D0=BE=D1=80=D0=BE=D1=82=D0=B5.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 5 +- .../org/mmu/tinkoffkinolab/CardActivity.java | 27 ++++++++-- .../org/mmu/tinkoffkinolab/MainActivity.java | 52 ++++++++++++++++++- app/src/main/res/layout/activity_main.xml | 52 ++++++++++++------- app/src/main/res/layout/fragment_card.xml | 1 + app/src/main/res/menu/toolbar_menu.xml | 7 ++- app/src/main/res/values-ru-rRU/strings.xml | 1 + app/src/main/res/values/strings.xml | 2 +- 8 files changed, 120 insertions(+), 27 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b8e82ee..1829328 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,6 +19,7 @@ android:name=".CardActivity" android:parentActivityName=".MainActivity" android:exported="false" + android:configChanges="orientation|screenSize" > + android:exported="true" + android:configChanges="orientation|screenSize" + > diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java b/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java index 5618baa..784c621 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java @@ -1,14 +1,17 @@ package org.mmu.tinkoffkinolab; import android.content.Context; +import android.content.res.Configuration; import android.os.AsyncTask; import android.os.Bundle; +import android.os.Debug; import android.util.AttributeSet; import android.util.Log; import android.view.MenuItem; import android.view.View; import android.widget.ImageView; import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -156,7 +159,8 @@ protected void onCreate(Bundle savedInstanceState) getSupportFragmentManager().registerFragmentLifecycleCallbacks(new FragmentManager.FragmentLifecycleCallbacks() { @Override - public void onFragmentViewCreated(@NonNull FragmentManager fm, @NonNull Fragment f, @NonNull View v, @Nullable Bundle savedInstanceState) + public void onFragmentViewCreated(@NonNull FragmentManager fm, @NonNull Fragment f, + @NonNull View v, @Nullable Bundle savedInstanceState) { imgPoster = v.findViewById(R.id.poster_image_view); txtHeader = v.findViewById(R.id.card_title); @@ -167,7 +171,24 @@ public void onFragmentViewCreated(@NonNull FragmentManager fm, @NonNull Fragment } }, false); } - + + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) + { + super.onConfigurationChanged(newConfig); + if (Debug.isDebuggerConnected()) + { + if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) + { + Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show(); + } + else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) + { + Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show(); + } + } + } + /** * Обработчик нажатия кнопки меню Назад * @@ -200,7 +221,7 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) private void fillCardUI() { // N.B.: параметры fit() и centerCrop() могут сильно замедлять загрузку! - Picasso.get().load(_cardData.get(Constants.ADAPTER_POSTER_PREVIEW_URL)).fit().centerCrop().into(imgPoster); + Picasso.get().load(_cardData.get(Constants.ADAPTER_POSTER_PREVIEW_URL)).into(imgPoster); txtHeader.setText(_cardData.get(Constants.ADAPTER_TITLE)); Objects.requireNonNull(getSupportActionBar()).setDisplayShowTitleEnabled(false); txtContent.setText(_cardData.get(Constants.ADAPTER_CONTENT)); diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java index 0062f95..203b751 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java @@ -11,6 +11,7 @@ import android.annotation.SuppressLint; import android.content.Intent; +import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; @@ -48,6 +49,7 @@ public class MainActivity extends AppCompatActivity { + //region 'Типы' private enum ViewMode @@ -184,9 +186,8 @@ protected void onPostExecute(Integer pagesCount) private boolean _isFiltered; private int _lastListViewPos; private int _lastListViewPos2; - private View scroller; - + private boolean _isLandscape; //endregion 'Поля и константы' @@ -253,6 +254,7 @@ protected void onCreate(Bundle savedInstanceState) if (_favouritesListFilePath.exists()) { loadFavouritesList(); + Log.d(LOG_TAG, "---------------------- состояние восстановлено! ----------------"); } if (Debug.isDebuggerConnected()) { @@ -276,9 +278,38 @@ protected void onCreate(Bundle savedInstanceState) this._initEventHandlers(); switchUIToPopularFilmsAsync(true, true); + Log.w(LOG_TAG, "End of onCreate() method ---------------------"); } + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) + { + super.onConfigurationChanged(newConfig); + if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) + { + _isLandscape = true; + if (Debug.isDebuggerConnected()) + { + Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show(); + } + } + else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) + { + _isLandscape = false; + if (Debug.isDebuggerConnected()) + { + Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show(); + } + } + inputPanel.setVisibility(_isLandscape ? View.GONE : View.VISIBLE); + if (inputPanel.getVisibility() == View.GONE) + { + Objects.requireNonNull(txtQuery.getText()).clear(); + } + invalidateOptionsMenu(); + } + /** * Обработчик сохранения состояния программы - если список Избранного пуст, то удаляет кеш изображений * @@ -305,10 +336,18 @@ protected void onSaveInstanceState(@NonNull Bundle outState) Log.d(LOG_TAG, "-------------------------- состояние сохранено! ----------------"); } + @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.toolbar_menu, menu); + //TODO: костылим перерисовку меню при смене ориентации, т.к. invalidate() не помогает?! + menu.findItem(R.id.action_switch_to_popular).setShowAsAction(_isLandscape ? + MenuItem.SHOW_AS_ACTION_ALWAYS : MenuItem.SHOW_AS_ACTION_IF_ROOM); + menu.findItem(R.id.action_switch_to_favorites).setShowAsAction(_isLandscape ? + MenuItem.SHOW_AS_ACTION_ALWAYS : MenuItem.SHOW_AS_ACTION_IF_ROOM); + menu.findItem(R.id.action_show_search_bar).setVisible(_isLandscape).setShowAsAction(_isLandscape ? + MenuItem.SHOW_AS_ACTION_ALWAYS : MenuItem.SHOW_AS_ACTION_NEVER); return super.onCreateOptionsMenu(menu); } @@ -351,6 +390,15 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) } break; } + case R.id.action_show_search_bar: + { + inputPanel.setVisibility(inputPanel.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE); + if (inputPanel.getVisibility() == View.GONE) + { + Objects.requireNonNull(txtQuery.getText()).clear(); + } + break; + } default: { Log.w(LOG_TAG, "Неизвестная команда меню!"); diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 83395cf..5ace045 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -65,8 +65,8 @@ - - - - - + - - + android:layout_height="match_parent" + android:orientation="vertical" + android:paddingLeft="5dp" + android:paddingRight="5dp" + android:scrollbarThumbVertical="@color/neo_light" + tools:ignore="SpeakableTextPresentCheck" + > + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_card.xml b/app/src/main/res/layout/fragment_card.xml index 4c00315..7364b4b 100644 --- a/app/src/main/res/layout/fragment_card.xml +++ b/app/src/main/res/layout/fragment_card.xml @@ -24,6 +24,7 @@ android:layout_width="match_parent" android:layout_height="400dp" android:layout_marginBottom="10dp" + android:scaleType="centerCrop" android:contentDescription="@string/filmposter_image_alt_text" tools:srcCompat="@tools:sample/backgrounds/scenic" /> diff --git a/app/src/main/res/menu/toolbar_menu.xml b/app/src/main/res/menu/toolbar_menu.xml index 72eff4c..254c70e 100644 --- a/app/src/main/res/menu/toolbar_menu.xml +++ b/app/src/main/res/menu/toolbar_menu.xml @@ -21,9 +21,14 @@ android:id="@+id/action_refresh" android:icon="@android:drawable/ic_menu_rotate" android:title="@string/action_refresh_title" - app:showAsAction="ifRoom"/> + app:showAsAction="never"/> + \ No newline at end of file diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index ed1c40b..c374711 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -10,4 +10,5 @@ Очистить список Вы уверены, что хотите удалить ВСЕ записи из списка? Подтверждение + Показать панель поиска \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 29b027f..1b0ca27 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,5 +11,5 @@ Are you sure want to delete all items? Confirmation - Hello blank fragment + Show search bar \ No newline at end of file From 56e130a452810259f56ca63f08d7e0f69637e897 Mon Sep 17 00:00:00 2001 From: Mikhail M Date: Thu, 23 Feb 2023 02:30:32 +0300 Subject: [PATCH 05/13] =?UTF-8?q?CardActivity.java:=20=D0=94=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6?= =?UTF-8?q?=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=BE=D1=82=D0=BA=D1=80=D1=8B?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BF=D0=BE=D1=81=D1=82=D0=B5=D1=80=20=D1=84?= =?UTF-8?q?=D0=B8=D0=BB=D1=8C=D0=BC=D0=B0=20=D0=BD=D0=B0=20=D0=B2=D0=B5?= =?UTF-8?q?=D1=81=D1=8C=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=20(=D0=BD=D1=83?= =?UTF-8?q?=D0=B6=D0=BD=D0=BE=20=D0=BF=D1=80=D0=B8=20FullHD+).=20=D0=9E?= =?UTF-8?q?=D0=BF=D1=82=D0=B8=D0=BC=D0=B8=D0=B7=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BB=20=D1=80=D0=B0=D0=B7=D0=BC=D0=B5=D1=80=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=B5=D1=80=D0=B0.=20=D0=94=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=B0=D0=B2=D1=82=D0=BE=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=BA=D1=80=D1=83=D1=82=D0=BA=D1=83,=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D1=81=D0=BB=D1=83=D1=87=D0=B0=D0=B9=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=B3=D0=B4=D0=B0=20=D0=BA=D0=B0=D1=80=D1=82=D0=B8=D0=BD=D0=BA?= =?UTF-8?q?=D0=B0=20=D1=81=D1=80=D0=B0=D0=B7=D1=83=20=D0=B7=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=BC=D0=B0=D0=B5=D1=82=20=D0=BF=D0=BE=D1=87=D1=82=D0=B8?= =?UTF-8?q?=20=D0=B2=D0=B5=D1=81=D1=8C=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/mmu/tinkoffkinolab/CardActivity.java | 73 +++++++++++---- .../org/mmu/tinkoffkinolab/MainActivity.java | 2 + .../java/org/mmu/tinkoffkinolab/Utils.java | 74 +++++++++++++++ app/src/main/res/layout/activity_main.xml | 11 ++- app/src/main/res/layout/fragment_card.xml | 52 ++++++----- app/src/main/res/values/strings.xml | 89 +++++++++++++++++++ 6 files changed, 255 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java b/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java index 784c621..1d0faee 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java @@ -1,15 +1,17 @@ package org.mmu.tinkoffkinolab; -import android.content.Context; +import static org.mmu.tinkoffkinolab.Constants.LOG_TAG; + import android.content.res.Configuration; +import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Debug; -import android.util.AttributeSet; import android.util.Log; import android.view.MenuItem; import android.view.View; import android.widget.ImageView; +import android.widget.ScrollView; import android.widget.TextView; import android.widget.Toast; @@ -21,6 +23,7 @@ import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.snackbar.Snackbar; +import com.squareup.picasso.Callback; import com.squareup.picasso.Picasso; import java.util.AbstractMap; @@ -45,6 +48,8 @@ public class CardActivity extends AppCompatActivity private TextView txtHeader; private TextView txtContent; private View androidContentView; + private View progBar; + private boolean _isHorizontal; //endregion 'Поля и константы' @@ -55,7 +60,6 @@ public class CardActivity extends AppCompatActivity private class WebDataDownloadTask extends AsyncTask { private final FilmsApi filmsApi; - private final View progBar = findViewById(R.id.progress_bar); private Map.Entry error; public Map.Entry getError() @@ -73,7 +77,7 @@ protected void onPreExecute() { super.onPreExecute(); progBar.setVisibility(View.VISIBLE); - Log.d(Constants.LOG_TAG, "Начало загрузки веб-ресурса..."); + Log.d(LOG_TAG, "Начало загрузки веб-ресурса..."); } @Override @@ -108,7 +112,7 @@ protected Void doInBackground(Void... unused) mes += String.format(Locale.ROOT, " %s (ErrorCode: %d), ResponseHeaders: \n%s\n ResponseBody: \n%s\n", Constants.KINO_API_ERROR_MES, apiEx.getCode(), headersText, apiEx.getResponseBody()); } - Log.e(Constants.LOG_TAG, mes.isEmpty() ? Constants.UNKNOWN_WEB_ERROR_MES : mes, ex); + Log.e(LOG_TAG, mes.isEmpty() ? Constants.UNKNOWN_WEB_ERROR_MES : mes, ex); } return null; } @@ -130,7 +134,7 @@ protected void onPostExecute(Void unused) } else { - Log.d(Constants.LOG_TAG, "Загрузка Веб-ресурса завершена успешно"); + Log.d(LOG_TAG, "Загрузка Веб-ресурса завершена успешно"); fillCardUI(); } } @@ -154,6 +158,8 @@ protected void onCreate(Bundle savedInstanceState) this.setSupportActionBar(customToolBar); Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayShowHomeEnabled(true); + progBar = findViewById(R.id.progress_bar); + filmId = getIntent().getStringExtra(Constants.ADAPTER_FILM_ID); getSupportFragmentManager().registerFragmentLifecycleCallbacks(new FragmentManager.FragmentLifecycleCallbacks() @@ -172,20 +178,20 @@ public void onFragmentViewCreated(@NonNull FragmentManager fm, @NonNull Fragment }, false); } + @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); - if (Debug.isDebuggerConnected()) + if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { - if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) - { - Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show(); - } - else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) - { - Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show(); - } + _isHorizontal = true; + //Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show(); + } + else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) + { + _isHorizontal = false; + //Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show(); } } @@ -220,11 +226,40 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) */ private void fillCardUI() { + progBar.setVisibility(View.VISIBLE); // N.B.: параметры fit() и centerCrop() могут сильно замедлять загрузку! - Picasso.get().load(_cardData.get(Constants.ADAPTER_POSTER_PREVIEW_URL)).into(imgPoster); - txtHeader.setText(_cardData.get(Constants.ADAPTER_TITLE)); - Objects.requireNonNull(getSupportActionBar()).setDisplayShowTitleEnabled(false); - txtContent.setText(_cardData.get(Constants.ADAPTER_CONTENT)); + final var posterUrl = _cardData.get(Constants.ADAPTER_POSTER_PREVIEW_URL); + Picasso.get().load(posterUrl).into(imgPoster, new Callback() + { + @Override + public void onSuccess() + { + progBar.setVisibility(View.GONE); + Objects.requireNonNull(getSupportActionBar()).setDisplayShowTitleEnabled(false); + final var textContent = _cardData.getOrDefault(Constants.ADAPTER_CONTENT, ""); + //noinspection ConstantConditions + txtHeader.setText(Objects.requireNonNullElse(_cardData.get(Constants.ADAPTER_TITLE), + textContent)); + txtContent.setText(textContent); + imgPoster.setOnClickListener(v1 -> { + Utils.showFullScreenPhoto(Uri.parse(posterUrl), v1.getContext()); + }); + final ScrollView sv = findViewById(R.id.fragment_scroll); + sv.postDelayed(() -> { + if (!Utils.isViewOnScreen(txtContent)) + { + sv.smoothScrollTo(0, imgPoster.getHeight() / 2); + } + }, 500); + } + + @Override + public void onError(Exception e) + { + progBar.setVisibility(View.GONE); + Log.e(LOG_TAG, "Ошибка загрузки большого постера", e); + } + }); } /** diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java index 203b751..ad9be95 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java @@ -346,6 +346,8 @@ public boolean onCreateOptionsMenu(Menu menu) MenuItem.SHOW_AS_ACTION_ALWAYS : MenuItem.SHOW_AS_ACTION_IF_ROOM); menu.findItem(R.id.action_switch_to_favorites).setShowAsAction(_isLandscape ? MenuItem.SHOW_AS_ACTION_ALWAYS : MenuItem.SHOW_AS_ACTION_IF_ROOM); + menu.findItem(R.id.action_go_to_top).setShowAsAction(_isLandscape ? + MenuItem.SHOW_AS_ACTION_ALWAYS : MenuItem.SHOW_AS_ACTION_IF_ROOM); menu.findItem(R.id.action_show_search_bar).setVisible(_isLandscape).setShowAsAction(_isLandscape ? MenuItem.SHOW_AS_ACTION_ALWAYS : MenuItem.SHOW_AS_ACTION_NEVER); return super.onCreateOptionsMenu(menu); diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/Utils.java b/app/src/main/java/org/mmu/tinkoffkinolab/Utils.java index bfb6dcb..4296acc 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/Utils.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/Utils.java @@ -2,11 +2,19 @@ import static org.mmu.tinkoffkinolab.Constants.LOG_TAG; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; +import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Build; import android.util.Log; +import android.view.View; import android.widget.ImageView; import java.io.BufferedInputStream; @@ -70,4 +78,70 @@ public static void extractImageToDiskCache(ImageView imgViewSource, String cache Log.e(LOG_TAG, "Ошибка записи в файл:\n " + cachedImageFilePath, e); } } + + public static void switchToFullScreen(Activity context) + { + // BEGIN_INCLUDE (get_current_ui_flags) + // The UI options currently enabled are represented by a bitfield. + // getSystemUiVisibility() gives us that bitfield. + int uiOptions = context.getWindow().getDecorView().getSystemUiVisibility(); + int newUiOptions = uiOptions; + // END_INCLUDE (get_current_ui_flags) + // BEGIN_INCLUDE (toggle_ui_flags) + boolean isImmersiveModeEnabled = ((uiOptions | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) == uiOptions); + if (isImmersiveModeEnabled) + { + Log.d(LOG_TAG, "Turning immersive mode mode off. "); + } + else + { + Log.d(LOG_TAG, "Turning immersive mode mode on."); + } + + // Navigation bar hiding: Backwards compatible to ICS. + newUiOptions ^= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; + + // Status bar hiding: Backwards compatible to Jellybean + newUiOptions ^= View.SYSTEM_UI_FLAG_FULLSCREEN; + + // Immersive mode: Backward compatible to KitKat. + // Note that this flag doesn't do anything by itself, it only augments the behavior + // of HIDE_NAVIGATION and FLAG_FULLSCREEN. For the purposes of this sample + // all three flags are being toggled together. + // Note that there are two immersive mode UI flags, one of which is referred to as "sticky". + // Sticky immersive mode differs in that it makes the navigation and status bars + // semi-transparent, and the UI flag does not get cleared when the user interacts with + // the screen. + newUiOptions ^= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + + context.getWindow().getDecorView().setSystemUiVisibility(newUiOptions); + //END_INCLUDE (set_ui_flags) + } + + public static void showFullScreenPhoto(Uri photoUri, Activity parentActivity) + { + showFullScreenPhoto(photoUri, parentActivity.getBaseContext()); + } + + public static void showFullScreenPhoto(Uri photoUri, Context context) + { + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_VIEW); + intent.setDataAndType(photoUri, "image/*"); + context.startActivity(intent); + } + + public static boolean isViewOnScreen(View target) + { + if (!target.isShown()) + { + return false; + } + final var actualPosition = new Rect(); + final var isGlobalVisible = target.getGlobalVisibleRect(actualPosition); + final var screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels; + final var screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels; + final var screen = new Rect(0, 0, screenWidth, screenHeight); + return isGlobalVisible && Rect.intersects(actualPosition, screen); + } } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 5ace045..9d3db60 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -66,9 +66,11 @@ - @@ -104,9 +108,10 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_card.xml b/app/src/main/res/layout/fragment_card.xml index 7364b4b..6f0375d 100644 --- a/app/src/main/res/layout/fragment_card.xml +++ b/app/src/main/res/layout/fragment_card.xml @@ -1,33 +1,38 @@ - + android:layout_height="wrap_content" + android:scrollbarThumbVertical="@color/neo_light" + android:scrollbarStyle="outsideOverlay" + android:fadeScrollbars="true" + > - - - - + android:layout_height="wrap_content" + android:orientation="vertical" + > - + - - - + android:layout_weight="1" + android:layout_marginBottom="10dp" + android:scaleType="centerCrop" + android:contentDescription="@string/filmposter_image_alt_text" + tools:srcCompat="@tools:sample/backgrounds/scenic" /> + - - - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1b0ca27..9c45cc4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,4 +12,93 @@ Confirmation Show search bar + + "Material is the metaphor.\n\n" + + "A material metaphor is the unifying theory of a rationalized space and a system of motion." + "The material is grounded in tactile reality, inspired by the study of paper and ink, yet " + "technologically advanced and open to imagination and magic.\n" + "Surfaces and edges of the material provide visual cues that are grounded in reality. The " + "use of familiar tactile attributes helps users quickly understand affordances. Yet the " + "flexibility of the material creates new affordances that supercede those in the physical " + "world, without breaking the rules of physics.\n" + "The fundamentals of light, surface, and movement are key to conveying how objects move, " + "interact, and exist in space and in relation to each other. Realistic lighting shows " + "seams, divides space, and indicates moving parts.\n\n" + + "Bold, graphic, intentional.\n\n" + + "The foundational elements of print based design typography, grids, space, scale, color, " + "and use of imagery guide visual treatments. These elements do far more than please the " + "eye. They create hierarchy, meaning, and focus. Deliberate color choices, edge to edge " + "imagery, large scale typography, and intentional white space create a bold and graphic " + "interface that immerse the user in the experience.\n" + "An emphasis on user actions makes core functionality immediately apparent and provides " + "waypoints for the user.\n\n" + + "Motion provides meaning.\n\n" + + "Motion respects and reinforces the user as the prime mover. Primary user actions are " + "inflection points that initiate motion, transforming the whole design.\n" + "All action takes place in a single environment. Objects are presented to the user without " + "breaking the continuity of experience even as they transform and reorganize.\n" + "Motion is meaningful and appropriate, serving to focus attention and maintain continuity. " + "Feedback is subtle yet clear. Transitions are efficient yet coherent.\n\n" + + "3D world.\n\n" + + "The material environment is a 3D space, which means all objects have x, y, and z " + "dimensions. The z-axis is perpendicularly aligned to the plane of the display, with the " + "positive z-axis extending towards the viewer. Every sheet of material occupies a single " + "position along the z-axis and has a standard 1dp thickness.\n" + "On the web, the z-axis is used for layering and not for perspective. The 3D world is " + "emulated by manipulating the y-axis.\n\n" + + "Light and shadow.\n\n" + + "Within the material environment, virtual lights illuminate the scene. Key lights create " + "directional shadows, while ambient light creates soft shadows from all angles.\n" + "Shadows in the material environment are cast by these two light sources. In Android " + "development, shadows occur when light sources are blocked by sheets of material at " + "various positions along the z-axis. On the web, shadows are depicted by manipulating the " + "y-axis only. The following example shows the card with a height of 6dp.\n\n" + + "Resting elevation.\n\n" + + "All material objects, regardless of size, have a resting elevation, or default elevation " + "that does not change. If an object changes elevation, it should return to its resting " + "elevation as soon as possible.\n\n" + + "Component elevations.\n\n" + + "The resting elevation for a component type is consistent across apps (e.g., FAB elevation " + "does not vary from 6dp in one app to 16dp in another app).\n" + "Components may have different resting elevations across platforms, depending on the depth " + "of the environment (e.g., TV has a greater depth than mobile or desktop).\n\n" + + "Responsive elevation and dynamic elevation offsets.\n\n" + + "Some component types have responsive elevation, meaning they change elevation in response " + "to user input (e.g., normal, focused, and pressed) or system events. These elevation " + "changes are consistently implemented using dynamic elevation offsets.\n" + "Dynamic elevation offsets are the goal elevation that a component moves towards, relative " + "to the component’s resting state. They ensure that elevation changes are consistent " + "across actions and component types. For example, all components that lift on press have " + "the same elevation change relative to their resting elevation.\n" + "Once the input event is completed or cancelled, the component will return to its resting " + "elevation.\n\n" + + "Avoiding elevation interference.\n\n" + + "Components with responsive elevations may encounter other components as they move between " + "their resting elevations and dynamic elevation offsets. Because material cannot pass " + "through other material, components avoid interfering with one another any number of ways, " + "whether on a per component basis or using the entire app layout.\n" + "On a component level, components can move or be removed before they cause interference. " + "For example, a floating action button (FAB) can disappear or move off screen before a " + "user picks up a card, or it can move if a snackbar appears.\n" + "On the layout level, design your app layout to minimize opportunities for interference. " + "For example, position the FAB to one side of stream of a cards so the FAB won’t interfere " + "when a user tries to pick up one of cards.\n\n" + \ No newline at end of file From 5dc8049d9f5a7c63b159ace29723d5a4ea0db4f7 Mon Sep 17 00:00:00 2001 From: Mikhail M Date: Thu, 23 Feb 2023 17:07:02 +0300 Subject: [PATCH 06/13] =?UTF-8?q?MainActivity.java:=20=D0=A3=D0=BB=D1=83?= =?UTF-8?q?=D1=87=D1=88=D0=B8=D0=BB=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8E=20=D0=BF=D0=BE=D0=B2=D0=BE=D1=80=D0=BE?= =?UTF-8?q?=D1=82=D0=B0=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B0=20-=20?= =?UTF-8?q?=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20=D0=BA=D0=BE=D1=81=D1=82=D1=8B?= =?UTF-8?q?=D0=BB=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D1=83=D0=BD=D0=BA?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=20=D0=BC=D0=B5=D0=BD=D1=8E/=D1=82=D1=83?= =?UTF-8?q?=D0=BB=D0=B1=D0=B0=D1=80=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 1 - .../org/mmu/tinkoffkinolab/Constants.java | 1 + .../org/mmu/tinkoffkinolab/MainActivity.java | 92 ++++++++++++++----- .../org/mmu/tinkoffkinolab/ProgramState.java | 17 ++++ .../res/drawable/rounded_bottom_shape.xml | 12 +++ app/src/main/res/layout/activity_main.xml | 1 + app/src/main/res/menu/toolbar_menu.xml | 11 ++- app/src/main/res/values-night/themes.xml | 1 + 8 files changed, 105 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/org/mmu/tinkoffkinolab/ProgramState.java create mode 100644 app/src/main/res/drawable/rounded_bottom_shape.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1829328..751349a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,7 +28,6 @@ diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/Constants.java b/app/src/main/java/org/mmu/tinkoffkinolab/Constants.java index f98d814..8b2ee7c 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/Constants.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/Constants.java @@ -31,6 +31,7 @@ public class Constants public static final String ADAPTER_POSTER_PREVIEW_URL = "ImageUrl"; public static final String UNKNOWN_WEB_ERROR_MES = "Ошибка загрузки данных по сети:"; public static final String KINO_API_ERROR_MES = "Ошибка API KinoPoisk"; + public static final String SCROLL_POS = "scroll_pos"; enum TopFilmsType { diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java index ad9be95..bc2716b 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java @@ -18,6 +18,8 @@ import android.os.AsyncTask; import android.os.Bundle; import android.os.Debug; +import android.os.Parcel; +import android.os.Parcelable; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; @@ -33,12 +35,16 @@ import android.widget.Toast; import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.shape.CornerFamily; +import com.google.android.material.shape.CornerTreatment; +import com.google.android.material.shape.MaterialShapeDrawable; import com.google.android.material.snackbar.Snackbar; import com.google.android.material.textfield.TextInputEditText; import com.squareup.picasso.Callback; import com.squareup.picasso.Picasso; import java.io.*; +import java.security.cert.X509Certificate; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -157,6 +163,7 @@ protected void onPostExecute(Integer pagesCount) //region 'Поля и константы' private static final List> _cardList = new ArrayList<>(); + private static ViewMode _currentViewMode; public File _favouritesListFilePath; private File _imageCacheDirPath; @@ -181,13 +188,13 @@ protected void onPostExecute(Integer pagesCount) private TextInputEditText txtQuery; private View androidContentView; private LinearLayout cardsContainer; - private ViewMode _currentViewMode; private LayoutInflater _layoutInflater; private boolean _isFiltered; private int _lastListViewPos; private int _lastListViewPos2; private View scroller; private boolean _isLandscape; + //endregion 'Поля и константы' @@ -250,12 +257,22 @@ protected void onCreate(Bundle savedInstanceState) setContentView(R.layout.activity_main); customToolBar = findViewById(R.id.top_toolbar); this.setSupportActionBar(customToolBar); + // восстанавливаем список избранного из файла _favouritesListFilePath = new File(this.getFilesDir(), FAVOURITES_LIST_FILE_NAME); if (_favouritesListFilePath.exists()) { loadFavouritesList(); Log.d(LOG_TAG, "---------------------- состояние восстановлено! ----------------"); } + // закруглённые углы для верхней панели + MaterialShapeDrawable toolbarBackground = (MaterialShapeDrawable) customToolBar.getBackground(); + toolbarBackground.setShapeAppearanceModel( + toolbarBackground.getShapeAppearanceModel() + .toBuilder() + .setBottomRightCorner(CornerFamily.ROUNDED, 25) + .setBottomLeftCorner(CornerFamily.ROUNDED, 25) + .build() + ); if (Debug.isDebuggerConnected()) { Picasso.get().setIndicatorsEnabled(true); @@ -270,18 +287,43 @@ protected void onCreate(Bundle savedInstanceState) txtQuery = findViewById(R.id.txt_input); cardsContainer = findViewById(R.id.card_linear_lyaout); inputPanel = findViewById(R.id.input_panel); + //inputPanel.setBackgroundResource(R.drawable.rounded_bottom_shape); swipeRefreshContainer = findViewById(R.id.film_list_swipe_refresh_container); swipeRefreshContainer.setColorSchemeResources(R.color.biz, R.color.neo, R.color.neo_dark, R.color.purple_light); _imageCacheDirPath = new File(this.getCacheDir(), Constants.FAVOURITES_CASH_DIR_NAME); scroller = findViewById(R.id.card_scroller); - + _isLandscape = this.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; + this._initEventHandlers(); - switchUIToPopularFilmsAsync(true, true); - + if (savedInstanceState == null || _cardList.isEmpty()) + { + switchUIToPopularFilmsAsync(true, true); + } + else + { + fillCardListUIFrom(_currentPageNumber, _currentViewMode == ViewMode.POPULAR ? + _cardList : _currentViewMode == ViewMode.FAVOURITES ? + new ArrayList<>(getFavouritesMap().values()) : new ArrayList<>()); + } + if (_isLandscape) + { + onScreenRotate(); + } Log.w(LOG_TAG, "End of onCreate() method ---------------------"); } + private void onScreenRotate() + { + final var isBlank = Objects.requireNonNull(txtQuery.getText()).toString().isBlank(); + inputPanel.setVisibility(_isLandscape && isBlank ? View.GONE : View.VISIBLE); + } + + /** + * Обработчик поворота экрана + * + * @param newConfig The new device configuration. + */ @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { @@ -302,12 +344,7 @@ else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show(); } } - inputPanel.setVisibility(_isLandscape ? View.GONE : View.VISIBLE); - if (inputPanel.getVisibility() == View.GONE) - { - Objects.requireNonNull(txtQuery.getText()).clear(); - } - invalidateOptionsMenu(); + onScreenRotate(); } /** @@ -319,6 +356,8 @@ else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); + // TODO: Нужно как-то определять, когда происходит выход из программы. а не поворот, чтобы + // не перезаписывать файл при каждом повороте if (favouritesMap == null || favouritesMap.isEmpty()) { this.deleteFile(Constants.FAVOURITES_LIST_FILE_NAME); @@ -341,15 +380,6 @@ protected void onSaveInstanceState(@NonNull Bundle outState) public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.toolbar_menu, menu); - //TODO: костылим перерисовку меню при смене ориентации, т.к. invalidate() не помогает?! - menu.findItem(R.id.action_switch_to_popular).setShowAsAction(_isLandscape ? - MenuItem.SHOW_AS_ACTION_ALWAYS : MenuItem.SHOW_AS_ACTION_IF_ROOM); - menu.findItem(R.id.action_switch_to_favorites).setShowAsAction(_isLandscape ? - MenuItem.SHOW_AS_ACTION_ALWAYS : MenuItem.SHOW_AS_ACTION_IF_ROOM); - menu.findItem(R.id.action_go_to_top).setShowAsAction(_isLandscape ? - MenuItem.SHOW_AS_ACTION_ALWAYS : MenuItem.SHOW_AS_ACTION_IF_ROOM); - menu.findItem(R.id.action_show_search_bar).setVisible(_isLandscape).setShowAsAction(_isLandscape ? - MenuItem.SHOW_AS_ACTION_ALWAYS : MenuItem.SHOW_AS_ACTION_NEVER); return super.onCreateOptionsMenu(menu); } @@ -371,7 +401,6 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) } case R.id.action_refresh: { - Objects.requireNonNull(txtQuery.getText()).clear(); refreshUIContent(); break; } @@ -395,15 +424,11 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) case R.id.action_show_search_bar: { inputPanel.setVisibility(inputPanel.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE); - if (inputPanel.getVisibility() == View.GONE) - { - Objects.requireNonNull(txtQuery.getText()).clear(); - } break; } default: { - Log.w(LOG_TAG, "Неизвестная команда меню!"); + Log.w(LOG_TAG, "Неизвестная команда меню - действие не назначено!"); break; } } @@ -513,6 +538,22 @@ private View.OnClickListener getOnListItemClickListener(String id, String title) return (View v) -> showFilmCardActivity(id, title); } + private void onSearchBarVisibleChanged() + { + if (inputPanel.getVisibility() == View.GONE) + { + Log.d(LOG_TAG, "Поле поиска скрыто - очищаю ввод!"); + Objects.requireNonNull(txtQuery.getText()).clear(); + customToolBar.setElevation(10 * getResources().getDisplayMetrics().density); + //inputPanel.setBackgroundResource(0); + } + else + { + customToolBar.setElevation(0); + //inputPanel.setBackgroundResource(R.drawable.rounded_bottom_shape); + } + } + //endregion 'Обработчики' @@ -539,6 +580,7 @@ private void _initEventHandlers() txtQuery.addTextChangedListener(getSearchTextChangeWatcher()); // при прокрутке списка фильмов до конца подгружаем следующую страницу результатов (если есть) scroller.setOnScrollChangeListener(this::onScrollChange); + inputPanel.getViewTreeObserver().addOnGlobalLayoutListener(this::onSearchBarVisibleChanged); } /** diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/ProgramState.java b/app/src/main/java/org/mmu/tinkoffkinolab/ProgramState.java new file mode 100644 index 0000000..ff753bb --- /dev/null +++ b/app/src/main/java/org/mmu/tinkoffkinolab/ProgramState.java @@ -0,0 +1,17 @@ +package org.mmu.tinkoffkinolab; + +public class ProgramState +{ + private static boolean isFirstLaunch = true; + + + public static boolean getIsFirstLaunch() + { + return isFirstLaunch; + } + + public static void setIsFirstLaunch(boolean value) + { + ProgramState.isFirstLaunch = value; + } +} diff --git a/app/src/main/res/drawable/rounded_bottom_shape.xml b/app/src/main/res/drawable/rounded_bottom_shape.xml new file mode 100644 index 0000000..bd286ef --- /dev/null +++ b/app/src/main/res/drawable/rounded_bottom_shape.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 9d3db60..b42d047 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -31,6 +31,7 @@ android:id="@+id/input_panel" android:layout_width="match_parent" android:layout_height="wrap_content" + app:cardBackgroundColor="?colorOnPrimary" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/top_toolbar"> diff --git a/app/src/main/res/menu/toolbar_menu.xml b/app/src/main/res/menu/toolbar_menu.xml index 254c70e..7078676 100644 --- a/app/src/main/res/menu/toolbar_menu.xml +++ b/app/src/main/res/menu/toolbar_menu.xml @@ -12,6 +12,12 @@ android:icon="@drawable/baseline_favorite_24" android:title="@string/action_popular_title" app:showAsAction="ifRoom" /> + - \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index fd6267d..278a4c5 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -14,5 +14,6 @@ @drawable/back_outlined_24px ?colorOnPrimary + @color/black \ No newline at end of file From abc05625f8abf0083f2ad11c862f5efaf1a3a2eb Mon Sep 17 00:00:00 2001 From: Mikhail M Date: Fri, 24 Feb 2023 10:53:12 +0300 Subject: [PATCH 07/13] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B8=D1=81=D1=87=D0=B5=D0=B7=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B7=D0=B0=D0=B3=D0=BE=D0=BB=D0=BE?= =?UTF-8?q?=D0=B2=D0=BA=D0=BE=D0=B2=20=D1=82=D1=83=D0=BB=D0=B1=D0=B0=D1=80?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20=D0=BF=D0=BE=D0=B2?= =?UTF-8?q?=D0=BE=D1=80=D0=BE=D1=82=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/mmu/tinkoffkinolab/CardActivity.java | 4 +- .../org/mmu/tinkoffkinolab/MainActivity.java | 166 +++++++++--------- .../java/org/mmu/tinkoffkinolab/Utils.java | 16 ++ app/src/main/res/values/strings.xml | 89 ---------- 4 files changed, 99 insertions(+), 176 deletions(-) diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java b/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java index 1d0faee..f878192 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java @@ -236,10 +236,10 @@ public void onSuccess() { progBar.setVisibility(View.GONE); Objects.requireNonNull(getSupportActionBar()).setDisplayShowTitleEnabled(false); - final var textContent = _cardData.getOrDefault(Constants.ADAPTER_CONTENT, ""); //noinspection ConstantConditions txtHeader.setText(Objects.requireNonNullElse(_cardData.get(Constants.ADAPTER_TITLE), - textContent)); + getSupportActionBar().getTitle())); + final var textContent = _cardData.getOrDefault(Constants.ADAPTER_CONTENT, ""); txtContent.setText(textContent); imgPoster.setOnClickListener(v1 -> { Utils.showFullScreenPhoto(Uri.parse(posterUrl), v1.getContext()); diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java index bc2716b..a5d6f3c 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java @@ -1,7 +1,6 @@ package org.mmu.tinkoffkinolab; import static org.mmu.tinkoffkinolab.Constants.*; -import static org.mmu.tinkoffkinolab.Utils.*; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; @@ -18,8 +17,6 @@ import android.os.AsyncTask; import android.os.Bundle; import android.os.Debug; -import android.os.Parcel; -import android.os.Parcelable; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; @@ -35,16 +32,12 @@ import android.widget.Toast; import com.google.android.material.appbar.MaterialToolbar; -import com.google.android.material.shape.CornerFamily; -import com.google.android.material.shape.CornerTreatment; -import com.google.android.material.shape.MaterialShapeDrawable; import com.google.android.material.snackbar.Snackbar; import com.google.android.material.textfield.TextInputEditText; import com.squareup.picasso.Callback; import com.squareup.picasso.Picasso; import java.io.*; -import java.security.cert.X509Certificate; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -138,7 +131,7 @@ protected void onPostExecute(Integer pagesCount) swipeRefreshContainer.setRefreshing(false); if (error != null) { - _lastSnackBar = showErrorSnackBar(UNKNOWN_WEB_ERROR_MES); + lastSnackBar = showErrorSnackBar(UNKNOWN_WEB_ERROR_MES); } else { @@ -149,9 +142,9 @@ protected void onPostExecute(Integer pagesCount) { _currentPageNumber++; } - if (_lastSnackBar != null && _lastSnackBar.isShown()) + if (lastSnackBar != null && lastSnackBar.isShown()) { - _lastSnackBar.dismiss(); + lastSnackBar.dismiss(); } } } @@ -164,36 +157,35 @@ protected void onPostExecute(Integer pagesCount) //region 'Поля и константы' private static final List> _cardList = new ArrayList<>(); private static ViewMode _currentViewMode; - public File _favouritesListFilePath; - private File _imageCacheDirPath; - - private boolean isRus; - private MaterialToolbar customToolBar; - private Snackbar _lastSnackBar; - private ProgressBar progBar; - private CardView inputPanel; - private SwipeRefreshLayout swipeRefreshContainer; - private Integer _topFilmsPagesCount = 1; /** * Содержит номер страницы, которая будет запрошена * * @implSpec НЕ изменять - управляется классом {@link WebDataDownloadTask} */ - private int _currentPageNumber = 1; + private static int _currentPageNumber = 1; + private static int _topFilmsPagesCount = 1; + public File favouritesListFilePath; + private File imageCacheDirPath; + private boolean isRus; + private MaterialToolbar customToolBar; + private Snackbar lastSnackBar; + private ProgressBar progBar; + private CardView inputPanel; + private SwipeRefreshLayout swipeRefreshContainer; /** - * Обязательно пересоздавать задачу перед каждым вызовом! + * @apiNote Обязательно пересоздавать задачу перед каждым вызовом! */ private AsyncTask downloadTask; private TextInputEditText txtQuery; private View androidContentView; private LinearLayout cardsContainer; - private LayoutInflater _layoutInflater; - private boolean _isFiltered; - private int _lastListViewPos; - private int _lastListViewPos2; + private LayoutInflater layoutInflater; + private boolean isFiltered; + private int lastListViewPos; + private int lastListViewPos2; private View scroller; - private boolean _isLandscape; + private boolean isLandscape; //endregion 'Поля и константы' @@ -258,21 +250,14 @@ protected void onCreate(Bundle savedInstanceState) customToolBar = findViewById(R.id.top_toolbar); this.setSupportActionBar(customToolBar); // восстанавливаем список избранного из файла - _favouritesListFilePath = new File(this.getFilesDir(), FAVOURITES_LIST_FILE_NAME); - if (_favouritesListFilePath.exists()) + this.favouritesListFilePath = new File(this.getFilesDir(), FAVOURITES_LIST_FILE_NAME); + if (this.favouritesListFilePath.exists()) { loadFavouritesList(); Log.d(LOG_TAG, "---------------------- состояние восстановлено! ----------------"); } // закруглённые углы для верхней панели - MaterialShapeDrawable toolbarBackground = (MaterialShapeDrawable) customToolBar.getBackground(); - toolbarBackground.setShapeAppearanceModel( - toolbarBackground.getShapeAppearanceModel() - .toBuilder() - .setBottomRightCorner(CornerFamily.ROUNDED, 25) - .setBottomLeftCorner(CornerFamily.ROUNDED, 25) - .build() - ); + Utils.setRoundedBottomToolbarStyle(customToolBar, 25); if (Debug.isDebuggerConnected()) { Picasso.get().setIndicatorsEnabled(true); @@ -280,19 +265,19 @@ protected void onCreate(Bundle savedInstanceState) } isRus = Locale.getDefault().getLanguage().equalsIgnoreCase("ru"); - _layoutInflater = getLayoutInflater(); + layoutInflater = getLayoutInflater(); androidContentView = findViewById(android.R.id.content); - progBar = findViewById(R.id.progress_bar); - progBar.setVisibility(View.GONE); + this.progBar = findViewById(R.id.progress_bar); + this.progBar.setVisibility(View.GONE); txtQuery = findViewById(R.id.txt_input); cardsContainer = findViewById(R.id.card_linear_lyaout); inputPanel = findViewById(R.id.input_panel); //inputPanel.setBackgroundResource(R.drawable.rounded_bottom_shape); swipeRefreshContainer = findViewById(R.id.film_list_swipe_refresh_container); swipeRefreshContainer.setColorSchemeResources(R.color.biz, R.color.neo, R.color.neo_dark, R.color.purple_light); - _imageCacheDirPath = new File(this.getCacheDir(), Constants.FAVOURITES_CASH_DIR_NAME); + this.imageCacheDirPath = new File(this.getCacheDir(), Constants.FAVOURITES_CASH_DIR_NAME); scroller = findViewById(R.id.card_scroller); - _isLandscape = this.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; + isLandscape = this.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; this._initEventHandlers(); @@ -302,11 +287,21 @@ protected void onCreate(Bundle savedInstanceState) } else { - fillCardListUIFrom(_currentPageNumber, _currentViewMode == ViewMode.POPULAR ? - _cardList : _currentViewMode == ViewMode.FAVOURITES ? - new ArrayList<>(getFavouritesMap().values()) : new ArrayList<>()); + // обновляем заголовок в Тулбаре + List> sourceList = null; + if (_currentViewMode == ViewMode.FAVOURITES) + { + switchUIToFavouriteFilms(false); + sourceList = _cardList; + } + else if (_currentViewMode == ViewMode.POPULAR) + { + switchUIToPopularFilmsAsync(false,false); + sourceList = new ArrayList<>(getFavouritesMap().values()); + } + fillCardListUIFrom(_currentPageNumber, Objects.requireNonNull(sourceList)); } - if (_isLandscape) + if (isLandscape) { onScreenRotate(); } @@ -316,7 +311,7 @@ protected void onCreate(Bundle savedInstanceState) private void onScreenRotate() { final var isBlank = Objects.requireNonNull(txtQuery.getText()).toString().isBlank(); - inputPanel.setVisibility(_isLandscape && isBlank ? View.GONE : View.VISIBLE); + this.inputPanel.setVisibility(isLandscape && isBlank ? View.GONE : View.VISIBLE); } /** @@ -330,7 +325,7 @@ public void onConfigurationChanged(@NonNull Configuration newConfig) super.onConfigurationChanged(newConfig); if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { - _isLandscape = true; + isLandscape = true; if (Debug.isDebuggerConnected()) { Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show(); @@ -338,7 +333,7 @@ public void onConfigurationChanged(@NonNull Configuration newConfig) } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { - _isLandscape = false; + isLandscape = false; if (Debug.isDebuggerConnected()) { Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show(); @@ -361,10 +356,10 @@ protected void onSaveInstanceState(@NonNull Bundle outState) if (favouritesMap == null || favouritesMap.isEmpty()) { this.deleteFile(Constants.FAVOURITES_LIST_FILE_NAME); - if (_imageCacheDirPath.exists()) + if (this.imageCacheDirPath.exists()) { //noinspection ResultOfMethodCallIgnored - Arrays.stream(Objects.requireNonNull(_imageCacheDirPath.listFiles())).parallel() + Arrays.stream(Objects.requireNonNull(this.imageCacheDirPath.listFiles())).parallel() .forEach(File::delete); } } @@ -423,7 +418,8 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) } case R.id.action_show_search_bar: { - inputPanel.setVisibility(inputPanel.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE); + this.inputPanel.setVisibility(this.inputPanel.getVisibility() == View.VISIBLE ? + View.GONE : View.VISIBLE); break; } default: @@ -466,7 +462,7 @@ private View.OnClickListener getOnLikeButtonClickListener(Map ca private void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { boolean isBottomReached = cardsContainer.getBottom() - v.getBottom() - scrollY == 0; - if (isBottomReached && !_isFiltered) + if (isBottomReached && !isFiltered) { if (_currentViewMode == ViewMode.POPULAR && _currentPageNumber < _topFilmsPagesCount) { @@ -475,11 +471,11 @@ private void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, in } else if (scrollY > 20) { - inputPanel.setCardElevation(10 * getResources().getDisplayMetrics().density); + this.inputPanel.setCardElevation(10 * getResources().getDisplayMetrics().density); } else if (scrollY == 0) { - inputPanel.setCardElevation(0); + this.inputPanel.setCardElevation(0); } } @@ -540,17 +536,15 @@ private View.OnClickListener getOnListItemClickListener(String id, String title) private void onSearchBarVisibleChanged() { - if (inputPanel.getVisibility() == View.GONE) + if (this.inputPanel.getVisibility() == View.GONE) { Log.d(LOG_TAG, "Поле поиска скрыто - очищаю ввод!"); Objects.requireNonNull(txtQuery.getText()).clear(); - customToolBar.setElevation(10 * getResources().getDisplayMetrics().density); - //inputPanel.setBackgroundResource(0); + this.customToolBar.setElevation(10 * getResources().getDisplayMetrics().density); } else { - customToolBar.setElevation(0); - //inputPanel.setBackgroundResource(R.drawable.rounded_bottom_shape); + this.customToolBar.setElevation(0); } } @@ -566,39 +560,42 @@ private void onSearchBarVisibleChanged() private void _initEventHandlers() { // обновляем страницу свайпом сверху - swipeRefreshContainer.setOnRefreshListener(this::refreshUIContent); + this.swipeRefreshContainer.setOnRefreshListener(this::refreshUIContent); // ищем текст по кнопке ВВОД на клавиатуре - txtQuery.setOnEditorActionListener((v, actionId, event) -> { - // N.B. Похоже, только для действия Done можно реализовать автоскрытие клавиатуры - при остальных клава остаётся на экране после клика - if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_ACTION_GO || actionId == EditorInfo.IME_ACTION_SEARCH) + this.txtQuery.setOnEditorActionListener((v, actionId, event) -> { + // N.B. Похоже, только для действия Done можно реализовать авто скрытие клавиатуры - при + // остальных клава остаётся на экране после клика + if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_ACTION_GO || + actionId == EditorInfo.IME_ACTION_SEARCH) { return false; } return true; }); // Обработчик изменения текста в поисковом контроле - txtQuery.addTextChangedListener(getSearchTextChangeWatcher()); + this.txtQuery.addTextChangedListener(getSearchTextChangeWatcher()); // при прокрутке списка фильмов до конца подгружаем следующую страницу результатов (если есть) - scroller.setOnScrollChangeListener(this::onScrollChange); - inputPanel.getViewTreeObserver().addOnGlobalLayoutListener(this::onSearchBarVisibleChanged); + this.scroller.setOnScrollChangeListener(this::onScrollChange); + // при скрытии панели Поиска очищаем текст фильтра + this.inputPanel.getViewTreeObserver().addOnGlobalLayoutListener(this::onSearchBarVisibleChanged); } /** * Метод показа (вывода из невидимости) всех карточек фильмов * * @apiNote Вызов имеет смысл, только если перед этим был вызов {@link #filterFilmCardsUI(String, Stream)} )} - * @implNote Сбрасывает признак фильтрации списка {@link #_isFiltered} + * @implNote Сбрасывает признак фильтрации списка {@link #isFiltered} */ private void showFilmCardsUI() { - if (_isFiltered) + if (isFiltered) { for (int i = 0; i < cardsContainer.getChildCount(); i++) { cardsContainer.getChildAt(i).setVisibility(View.VISIBLE); } } - _isFiltered = false; + isFiltered = false; } /** @@ -641,7 +638,7 @@ private void filterFilmCardsUI(String query, Stream> cardStr } } } - _isFiltered = true; + isFiltered = true; } /** @@ -675,7 +672,7 @@ private void fillCardListUIFrom(int startItemIndex, List> ca { for (int i = startItemIndex; i < cardList.size(); i++) { - @SuppressLint("InflateParams") final var listItem = _layoutInflater.inflate(R.layout.list_item, null); + @SuppressLint("InflateParams") final var listItem = layoutInflater.inflate(R.layout.list_item, null); final var cardData = cardList.get(i); final var id = cardData.get(Constants.ADAPTER_FILM_ID); ((TextView) listItem.findViewById(R.id.film_id_holder)).setText(id); @@ -737,7 +734,7 @@ public void onSuccess() { //TODO: без задержки, картинки не успевают прорисоваться, но слишком большая задержка, // тоже опасна - юзер уже мог переключить представление (хотя объект не должен уничтожаться). - imgView.postDelayed(() -> extractImageToDiskCache(imgView, cachedImageFilePath), 300); + imgView.postDelayed(() -> Utils.extractImageToDiskCache(imgView, cachedImageFilePath), 300); } @Override @@ -759,11 +756,11 @@ public void onError(Exception e) */ private boolean addToOrRemoveFromFavourites(String id, Map cardData, Drawable image) { - if (!_imageCacheDirPath.exists() && !_imageCacheDirPath.mkdir()) + if (!this.imageCacheDirPath.exists() && !this.imageCacheDirPath.mkdir()) { Log.w(LOG_TAG, "Ошибка создания подкаталога для кэша постеров к фильмам"); } - final var imgPreviewFilePath = new File(_imageCacheDirPath, "preview_" + id + ".webp"); + final var imgPreviewFilePath = new File(this.imageCacheDirPath, "preview_" + id + ".webp"); if (this.getFavouritesMap().containsKey(id)) { @@ -836,14 +833,13 @@ private void startTopFilmsDownloadTask(int pageNumber) */ private void switchUIToPopularFilmsAsync(boolean ifBeginFromPageOne, boolean isDownloadNew) { + customToolBar.setTitle(R.string.action_popular_title); if (_currentViewMode == ViewMode.POPULAR && !isDownloadNew) { return; } _currentViewMode = ViewMode.POPULAR; - customToolBar.setTitle(R.string.action_popular_title); - - _lastListViewPos2 = scroller.getScrollY(); + lastListViewPos2 = scroller.getScrollY(); if (ifBeginFromPageOne && isDownloadNew) { @@ -868,7 +864,7 @@ else if (!isDownloadNew) if (query.isBlank()) { // на основе кода отсюда: https://stackoverflow.com/a/3263540/2323972 - scroller.post(() -> scroller.scrollTo(0, _lastListViewPos)); + scroller.post(() -> scroller.scrollTo(0, lastListViewPos)); } else { @@ -887,17 +883,17 @@ private void switchUIToFavouriteFilms() */ private void switchUIToFavouriteFilms(boolean forceRefresh) { + customToolBar.setTitle(R.string.action_favourites_title); if (!forceRefresh && _currentViewMode == ViewMode.FAVOURITES) { return; } if (!forceRefresh) { - _lastListViewPos = scroller.getScrollY(); + lastListViewPos = scroller.getScrollY(); } _currentViewMode = ViewMode.FAVOURITES; clearList(true); - customToolBar.setTitle(R.string.action_favourites_title); if (!getFavouritesMap().isEmpty()) { fillCardListUIFrom(0, new ArrayList<>(getFavouritesMap().values())); @@ -908,7 +904,7 @@ private void switchUIToFavouriteFilms(boolean forceRefresh) } else if (!forceRefresh) { - scroller.post(() -> scroller.scrollTo(0, _lastListViewPos2)); + scroller.post(() -> scroller.scrollTo(0, lastListViewPos2)); } } } @@ -953,7 +949,7 @@ private void showFilmCardActivity(String kinoApiFilmId, String cardTitle) private void loadFavouritesList() { final var tempValuesMap = new HashMap(); - try (var fr = new BufferedReader(new FileReader(_favouritesListFilePath)); + try (var fr = new BufferedReader(new FileReader(this.favouritesListFilePath)); var fileLines = fr.lines()) { fileLines.forEach(line -> { @@ -981,7 +977,7 @@ else if (!line.equalsIgnoreCase(FILM_START_TOKEN)) */ private void saveFavouritesList() { - try (BufferedWriter fw = new BufferedWriter(new FileWriter(_favouritesListFilePath))) + try (BufferedWriter fw = new BufferedWriter(new FileWriter(this.favouritesListFilePath))) { favouritesMap.forEach((id, filmData) -> { try @@ -1006,7 +1002,7 @@ private void saveFavouritesList() } catch (IOException | RuntimeException e) { - Log.e(LOG_TAG, "Ошибка записи файла: " + _favouritesListFilePath.toString(), e); + Log.e(LOG_TAG, "Ошибка записи файла: " + this.favouritesListFilePath.toString(), e); } } diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/Utils.java b/app/src/main/java/org/mmu/tinkoffkinolab/Utils.java index 4296acc..e97c9dd 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/Utils.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/Utils.java @@ -17,6 +17,10 @@ import android.view.View; import android.widget.ImageView; +import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.shape.CornerFamily; +import com.google.android.material.shape.MaterialShapeDrawable; + import java.io.BufferedInputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -144,4 +148,16 @@ public static boolean isViewOnScreen(View target) final var screen = new Rect(0, 0, screenWidth, screenHeight); return isGlobalVisible && Rect.intersects(actualPosition, screen); } + + public static void setRoundedBottomToolbarStyle(MaterialToolbar customToolBar, int cornerRadius) + { + final var toolbarBackground = (MaterialShapeDrawable) customToolBar.getBackground(); + toolbarBackground.setShapeAppearanceModel( + toolbarBackground.getShapeAppearanceModel() + .toBuilder() + .setBottomRightCorner(CornerFamily.ROUNDED, cornerRadius) + .setBottomLeftCorner(CornerFamily.ROUNDED, cornerRadius) + .build() + ); + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9c45cc4..1b0ca27 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,93 +12,4 @@ Confirmation Show search bar - - "Material is the metaphor.\n\n" - - "A material metaphor is the unifying theory of a rationalized space and a system of motion." - "The material is grounded in tactile reality, inspired by the study of paper and ink, yet " - "technologically advanced and open to imagination and magic.\n" - "Surfaces and edges of the material provide visual cues that are grounded in reality. The " - "use of familiar tactile attributes helps users quickly understand affordances. Yet the " - "flexibility of the material creates new affordances that supercede those in the physical " - "world, without breaking the rules of physics.\n" - "The fundamentals of light, surface, and movement are key to conveying how objects move, " - "interact, and exist in space and in relation to each other. Realistic lighting shows " - "seams, divides space, and indicates moving parts.\n\n" - - "Bold, graphic, intentional.\n\n" - - "The foundational elements of print based design typography, grids, space, scale, color, " - "and use of imagery guide visual treatments. These elements do far more than please the " - "eye. They create hierarchy, meaning, and focus. Deliberate color choices, edge to edge " - "imagery, large scale typography, and intentional white space create a bold and graphic " - "interface that immerse the user in the experience.\n" - "An emphasis on user actions makes core functionality immediately apparent and provides " - "waypoints for the user.\n\n" - - "Motion provides meaning.\n\n" - - "Motion respects and reinforces the user as the prime mover. Primary user actions are " - "inflection points that initiate motion, transforming the whole design.\n" - "All action takes place in a single environment. Objects are presented to the user without " - "breaking the continuity of experience even as they transform and reorganize.\n" - "Motion is meaningful and appropriate, serving to focus attention and maintain continuity. " - "Feedback is subtle yet clear. Transitions are efficient yet coherent.\n\n" - - "3D world.\n\n" - - "The material environment is a 3D space, which means all objects have x, y, and z " - "dimensions. The z-axis is perpendicularly aligned to the plane of the display, with the " - "positive z-axis extending towards the viewer. Every sheet of material occupies a single " - "position along the z-axis and has a standard 1dp thickness.\n" - "On the web, the z-axis is used for layering and not for perspective. The 3D world is " - "emulated by manipulating the y-axis.\n\n" - - "Light and shadow.\n\n" - - "Within the material environment, virtual lights illuminate the scene. Key lights create " - "directional shadows, while ambient light creates soft shadows from all angles.\n" - "Shadows in the material environment are cast by these two light sources. In Android " - "development, shadows occur when light sources are blocked by sheets of material at " - "various positions along the z-axis. On the web, shadows are depicted by manipulating the " - "y-axis only. The following example shows the card with a height of 6dp.\n\n" - - "Resting elevation.\n\n" - - "All material objects, regardless of size, have a resting elevation, or default elevation " - "that does not change. If an object changes elevation, it should return to its resting " - "elevation as soon as possible.\n\n" - - "Component elevations.\n\n" - - "The resting elevation for a component type is consistent across apps (e.g., FAB elevation " - "does not vary from 6dp in one app to 16dp in another app).\n" - "Components may have different resting elevations across platforms, depending on the depth " - "of the environment (e.g., TV has a greater depth than mobile or desktop).\n\n" - - "Responsive elevation and dynamic elevation offsets.\n\n" - - "Some component types have responsive elevation, meaning they change elevation in response " - "to user input (e.g., normal, focused, and pressed) or system events. These elevation " - "changes are consistently implemented using dynamic elevation offsets.\n" - "Dynamic elevation offsets are the goal elevation that a component moves towards, relative " - "to the component’s resting state. They ensure that elevation changes are consistent " - "across actions and component types. For example, all components that lift on press have " - "the same elevation change relative to their resting elevation.\n" - "Once the input event is completed or cancelled, the component will return to its resting " - "elevation.\n\n" - - "Avoiding elevation interference.\n\n" - - "Components with responsive elevations may encounter other components as they move between " - "their resting elevations and dynamic elevation offsets. Because material cannot pass " - "through other material, components avoid interfering with one another any number of ways, " - "whether on a per component basis or using the entire app layout.\n" - "On a component level, components can move or be removed before they cause interference. " - "For example, a floating action button (FAB) can disappear or move off screen before a " - "user picks up a card, or it can move if a snackbar appears.\n" - "On the layout level, design your app layout to minimize opportunities for interference. " - "For example, position the FAB to one side of stream of a cards so the FAB won’t interfere " - "when a user tries to pick up one of cards.\n\n" - \ No newline at end of file From b10e0e70ab14fd2fd764feef638167bd3411da3e Mon Sep 17 00:00:00 2001 From: Mikhail M Date: Fri, 24 Feb 2023 16:07:22 +0300 Subject: [PATCH 08/13] =?UTF-8?q?=D0=A0=D0=B5=D1=84=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D0=BD=D0=B3:=20=D1=80=D0=B0=D1=81=D0=BA?= =?UTF-8?q?=D0=B8=D0=B4=D0=B0=D0=BB=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=20onC?= =?UTF-8?q?reate()=20=D0=BD=D0=B0=20=D0=BD=D0=B5=D1=81=D0=BA=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=BA=D0=BE=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D1=87=D0=B8=D0=BA=D0=BE=D0=B2,=20=D0=B2=20=D1=82.=D1=87.=20onR?= =?UTF-8?q?estoreInstanceState().=20=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B1=D0=B0=D0=B3=20=D1=81=20=D0=B7=D0=B0=D0=B3?= =?UTF-8?q?=D1=80=D1=83=D0=B7=D0=BA=D0=BE=D0=B9=20=D1=81=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=D0=BC=20UI=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D1=81=20=D1=82=D0=BE=D0=B9=20=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=86=D1=8B=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=B2=D0=BE=D1=80=D0=BE=D1=82=D0=B0=20=D1=83?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=BE=D0=B9=D1=81=D1=82=D0=B2=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/mmu/tinkoffkinolab/CardActivity.java | 11 +- .../mmu/tinkoffkinolab/FilmsApiHelper.java | 6 +- .../org/mmu/tinkoffkinolab/MainActivity.java | 218 +++++++++++------- app/src/main/res/layout/activity_main.xml | 197 ++++++++-------- 4 files changed, 255 insertions(+), 177 deletions(-) diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java b/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java index f878192..01669e7 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java @@ -86,7 +86,9 @@ protected Void doInBackground(Void... unused) try { final var response = filmsApi.apiV22FilmsIdGet(Integer.parseInt(filmId)); - _cardData.put(Constants.ADAPTER_TITLE, response.getNameRu()); + final var engName = Objects.requireNonNullElse(response.getNameEn(), ""); + _cardData.put(Constants.ADAPTER_TITLE, response.getNameRu() + (!engName.isBlank() ? + System.lineSeparator() + "[" + engName +"]" : "")); final var genres = "\n\n Жанры: " + response.getGenres().stream() .map(g -> g.getGenre() + ", ") .collect(Collectors.joining()) @@ -94,7 +96,8 @@ protected Void doInBackground(Void... unused) final var countries = "\n\n Страны: " + response.getCountries().stream() .map(country -> country.getCountry() + ", ") .collect(Collectors.joining()).replaceFirst(",\\s*$", ""); - final var res = response.getDescription() + genres + countries; + final var res = Objects.requireNonNullElse(response.getDescription(), "") + + genres + countries; _cardData.put(Constants.ADAPTER_CONTENT, res); _cardData.put(Constants.ADAPTER_POSTER_PREVIEW_URL, response.getPosterUrl()); } @@ -239,8 +242,8 @@ public void onSuccess() //noinspection ConstantConditions txtHeader.setText(Objects.requireNonNullElse(_cardData.get(Constants.ADAPTER_TITLE), getSupportActionBar().getTitle())); - final var textContent = _cardData.getOrDefault(Constants.ADAPTER_CONTENT, ""); - txtContent.setText(textContent); + final var textContent = _cardData.get(Constants.ADAPTER_CONTENT); + txtContent.setText(Objects.requireNonNullElse(textContent, "")); imgPoster.setOnClickListener(v1 -> { Utils.showFullScreenPhoto(Uri.parse(posterUrl), v1.getContext()); }); diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/FilmsApiHelper.java b/app/src/main/java/org/mmu/tinkoffkinolab/FilmsApiHelper.java index 116f566..9a0d5b9 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/FilmsApiHelper.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/FilmsApiHelper.java @@ -11,7 +11,11 @@ public class FilmsApiHelper public static FilmsApi getFilmsApi() { - return Objects.requireNonNullElse(filmsApi, createFilmsApi()); + if (filmsApi == null) + { + filmsApi = createFilmsApi(); + } + return filmsApi; } private static FilmsApi createFilmsApi() diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java index a5d6f3c..fe50cd5 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java @@ -60,11 +60,13 @@ private enum ViewMode private class WebDataDownloadTask extends AsyncTask { - private static final int FILMS_COUNT_PER_PAGE = 20; - private final FilmsApi filmsApi; - private Map.Entry error; private static final String UNKNOWN_WEB_ERROR_MES = "Ошибка загрузки данных по сети:"; private static final String KINO_API_ERROR_MES = "Ошибка API KinoPoisk"; + public static final int FILMS_COUNT_PER_PAGE = 20; + private final FilmsApi filmsApi; + private final ProgressBar refreshProgBar = findViewById(R.id.progress_bar_bottom); + private Map.Entry error; + public WebDataDownloadTask(FilmsApi engine) @@ -76,30 +78,82 @@ public WebDataDownloadTask(FilmsApi engine) protected void onPreExecute() { super.onPreExecute(); - progBar.setVisibility(View.VISIBLE); - progBar.bringToFront(); + if (_nextPageNumber == 1) + { + progBar.setVisibility(View.VISIBLE); + progBar.bringToFront(); + } + else + { + refreshProgBar.setVisibility(View.VISIBLE); + } Log.d(LOG_TAG, "Начало загрузки веб-ресурса..."); } @Override protected Integer doInBackground(Integer... pageNumber) + { + return downloadPopularFilmList(pageNumber[0]); + } + + @Override + protected void onPostExecute(Integer pagesCount) + { + super.onPostExecute(pagesCount); + progBar.setVisibility(View.GONE); + refreshProgBar.setVisibility(View.GONE); + swipeRefreshContainer.setRefreshing(false); + if (error != null) + { + lastSnackBar = showErrorSnackBar(UNKNOWN_WEB_ERROR_MES); + } + else + { + Log.d(LOG_TAG, "Загрузка Веб-ресурса завершена успешно"); + fillCardListUIFrom((_nextPageNumber - 1) * FILMS_COUNT_PER_PAGE, _cardList); + _topFilmsPagesCount = pagesCount; + // наращивать номер страницы можно ТОЛЬКО после успешной загрузки, иначе можно накрутить номер + if (_nextPageNumber < _topFilmsPagesCount) + { + _nextPageNumber++; + } + if (lastSnackBar != null && lastSnackBar.isShown()) + { + lastSnackBar.dismiss(); + } + } + } + + /** + * Метод загрузки списка популярных Фильмов + * + * @param pageNumber Номер страницы, с которой надо начинать загрузку + * + * @return Общее Кол-во страниц на сервере или 0 (если операция была прервана пользователем) + * + * @apiNote Не выбрасывает исключений + */ + private int downloadPopularFilmList(int pageNumber) { int res = 0; try { - final var response = filmsApi.apiV22FilmsTopGet(Constants.TopFilmsType.TOP_100_POPULAR_FILMS.name(), pageNumber[0]); + final var response = filmsApi.apiV22FilmsTopGet( + TopFilmsType.TOP_100_POPULAR_FILMS.name(), + pageNumber); res = response.getPagesCount(); for (var filmData : response.getFilms()) { if (isCancelled()) { - return null; + return 0; } final var id = String.valueOf(filmData.getFilmId()); Optional name = Optional.ofNullable(isRus ? filmData.getNameRu() : filmData.getNameEn()); _cardList.add(Map.of(Constants.ADAPTER_FILM_ID, id, Constants.ADAPTER_TITLE, name.orElse(id), - Constants.ADAPTER_CONTENT, filmData.getGenres().get(0).getGenre() + " (" + filmData.getYear() + ")", + Constants.ADAPTER_CONTENT, filmData.getGenres().get(0).getGenre() + + " (" + filmData.getYear() + ")", Constants.ADAPTER_POSTER_PREVIEW_URL, filmData.getPosterUrlPreview()) ); } @@ -122,32 +176,8 @@ protected Integer doInBackground(Integer... pageNumber) } return res; } - - @Override - protected void onPostExecute(Integer pagesCount) - { - super.onPostExecute(pagesCount); - progBar.setVisibility(View.GONE); - swipeRefreshContainer.setRefreshing(false); - if (error != null) - { - lastSnackBar = showErrorSnackBar(UNKNOWN_WEB_ERROR_MES); - } - else - { - Log.d(LOG_TAG, "Загрузка Веб-ресурса завершена успешно"); - fillCardListUIFrom((_currentPageNumber - 1) * FILMS_COUNT_PER_PAGE, _cardList); - _topFilmsPagesCount = pagesCount; - if (_currentPageNumber < _topFilmsPagesCount) - { - _currentPageNumber++; - } - if (lastSnackBar != null && lastSnackBar.isShown()) - { - lastSnackBar.dismiss(); - } - } - } + + } //endregion 'Типы' @@ -162,7 +192,7 @@ protected void onPostExecute(Integer pagesCount) * * @implSpec НЕ изменять - управляется классом {@link WebDataDownloadTask} */ - private static int _currentPageNumber = 1; + private static int _nextPageNumber = 1; private static int _topFilmsPagesCount = 1; public File favouritesListFilePath; @@ -237,6 +267,8 @@ public AlertDialog getConfirmClearDialog() /** * При создании Экрана навешиваем обработчик обновления списка свайпом * + * @apiNote Вызывается при запуске программы, а также, если процесс был вытеснен из памяти + * * @param savedInstanceState If the activity is being re-initialized after * previously being shut down then this Bundle contains the data it most * recently supplied in {@link #onSaveInstanceState}. Note: Otherwise it is null. @@ -245,7 +277,7 @@ public AlertDialog getConfirmClearDialog() protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - Log.w(LOG_TAG, "---------------- Start of onCreate() method"); + Log.d(LOG_TAG, "---------------- In the onCreate() method"); setContentView(R.layout.activity_main); customToolBar = findViewById(R.id.top_toolbar); this.setSupportActionBar(customToolBar); @@ -254,7 +286,7 @@ protected void onCreate(Bundle savedInstanceState) if (this.favouritesListFilePath.exists()) { loadFavouritesList(); - Log.d(LOG_TAG, "---------------------- состояние восстановлено! ----------------"); + Log.d(LOG_TAG, "Список избранного загружен из файла:\n " + this.favouritesListFilePath); } // закруглённые углы для верхней панели Utils.setRoundedBottomToolbarStyle(customToolBar, 25); @@ -263,49 +295,72 @@ protected void onCreate(Bundle savedInstanceState) Picasso.get().setIndicatorsEnabled(true); //Picasso.get().setLoggingEnabled(true); } - isRus = Locale.getDefault().getLanguage().equalsIgnoreCase("ru"); - - layoutInflater = getLayoutInflater(); - androidContentView = findViewById(android.R.id.content); + this.isRus = Locale.getDefault().getLanguage().equalsIgnoreCase("ru"); + + this.layoutInflater = getLayoutInflater(); + this.androidContentView = findViewById(android.R.id.content); this.progBar = findViewById(R.id.progress_bar); this.progBar.setVisibility(View.GONE); - txtQuery = findViewById(R.id.txt_input); - cardsContainer = findViewById(R.id.card_linear_lyaout); - inputPanel = findViewById(R.id.input_panel); + this.txtQuery = findViewById(R.id.txt_input); + this.cardsContainer = findViewById(R.id.card_linear_lyaout); + this.inputPanel = findViewById(R.id.input_panel); //inputPanel.setBackgroundResource(R.drawable.rounded_bottom_shape); - swipeRefreshContainer = findViewById(R.id.film_list_swipe_refresh_container); - swipeRefreshContainer.setColorSchemeResources(R.color.biz, R.color.neo, R.color.neo_dark, R.color.purple_light); + this.swipeRefreshContainer = findViewById(R.id.film_list_swipe_refresh_container); + this.swipeRefreshContainer.setColorSchemeResources(R.color.biz, R.color.neo, R.color.neo_dark, R.color.purple_light); this.imageCacheDirPath = new File(this.getCacheDir(), Constants.FAVOURITES_CASH_DIR_NAME); - scroller = findViewById(R.id.card_scroller); - isLandscape = this.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; + this.scroller = findViewById(R.id.card_scroller); + this.isLandscape = this.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; - this._initEventHandlers(); - - if (savedInstanceState == null || _cardList.isEmpty()) + this.setEventHandlers(); + + if (_cardList.isEmpty()) { switchUIToPopularFilmsAsync(true, true); } - else + Log.d(LOG_TAG, "Exit of onCreate() method ---------------------"); + } + + /** + * @apiNote Этот метод всегда вызывается после {@link #onCreate(Bundle)}, а также после + * {@link #onRestart()}, например после нажатия кнопки назад на Activity вызванной из этой. + */ + @Override + protected void onStart() + { + super.onStart(); + Log.d(LOG_TAG, "---------------- In the onStart() method"); + if (this.isLandscape) { - // обновляем заголовок в Тулбаре - List> sourceList = null; - if (_currentViewMode == ViewMode.FAVOURITES) - { - switchUIToFavouriteFilms(false); - sourceList = _cardList; - } - else if (_currentViewMode == ViewMode.POPULAR) - { - switchUIToPopularFilmsAsync(false,false); - sourceList = new ArrayList<>(getFavouritesMap().values()); - } - fillCardListUIFrom(_currentPageNumber, Objects.requireNonNull(sourceList)); + onScreenRotate(); } - if (isLandscape) + Log.w(LOG_TAG, "Exit of onStart() method ---------------------"); + } + + /** + * Метод восстановления состояния Активити - Вызывается после {@on + * + * @param savedInstanceState the data most recently supplied in {@link #onSaveInstanceState}. + * + */ + @Override + protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) + { + super.onRestoreInstanceState(savedInstanceState); + // обновляем заголовок в Тулбаре + List> sourceList = null; + if (_currentViewMode == ViewMode.FAVOURITES) { - onScreenRotate(); + switchUIToFavouriteFilms(false); + sourceList = new ArrayList<>(getFavouritesMap().values()); + } + else if (_currentViewMode == ViewMode.POPULAR) + { + switchUIToPopularFilmsAsync(false,false); + sourceList = _cardList; } - Log.w(LOG_TAG, "End of onCreate() method ---------------------"); + fillCardListUIFrom((_nextPageNumber - 2) * WebDataDownloadTask.FILMS_COUNT_PER_PAGE, + Objects.requireNonNull(sourceList)); + Log.d(LOG_TAG, "---------------------- состояние восстановлено! ----------------"); } private void onScreenRotate() @@ -464,7 +519,7 @@ private void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, in boolean isBottomReached = cardsContainer.getBottom() - v.getBottom() - scrollY == 0; if (isBottomReached && !isFiltered) { - if (_currentViewMode == ViewMode.POPULAR && _currentPageNumber < _topFilmsPagesCount) + if (_currentViewMode == ViewMode.POPULAR && _nextPageNumber < _topFilmsPagesCount) { switchUIToPopularFilmsAsync(false, true); } @@ -557,7 +612,7 @@ private void onSearchBarVisibleChanged() /** * Метод настройки событий виджетов */ - private void _initEventHandlers() + private void setEventHandlers() { // обновляем страницу свайпом сверху this.swipeRefreshContainer.setOnRefreshListener(this::refreshUIContent); @@ -647,11 +702,11 @@ private void filterFilmCardsUI(String query, Stream> cardStr private void refreshUIContent() { Objects.requireNonNull(txtQuery.getText()).clear(); - if (this._currentViewMode == ViewMode.POPULAR) + if (_currentViewMode == ViewMode.POPULAR) { switchUIToPopularFilmsAsync(true, true); } - else if (this._currentViewMode == ViewMode.FAVOURITES) + else if (_currentViewMode == ViewMode.FAVOURITES) { cardsContainer.setVisibility(View.INVISIBLE); switchUIToFavouriteFilms(true); @@ -672,7 +727,8 @@ private void fillCardListUIFrom(int startItemIndex, List> ca { for (int i = startItemIndex; i < cardList.size(); i++) { - @SuppressLint("InflateParams") final var listItem = layoutInflater.inflate(R.layout.list_item, null); + @SuppressLint("InflateParams") + final var listItem = layoutInflater.inflate(R.layout.list_item, null); final var cardData = cardList.get(i); final var id = cardData.get(Constants.ADAPTER_FILM_ID); ((TextView) listItem.findViewById(R.id.film_id_holder)).setText(id); @@ -797,7 +853,7 @@ private Snackbar showErrorSnackBar(String message) var popup = Snackbar.make(this.androidContentView, message, Snackbar.LENGTH_INDEFINITE); popup.setAction(R.string.repeat_button_caption, view -> { var text = Objects.requireNonNull(txtQuery.getText()).toString().replace("null", ""); - this.startTopFilmsDownloadTask(_currentPageNumber); + this.startTopFilmsDownloadTask(_nextPageNumber); popup.dismiss(); }); popup.show(); @@ -829,9 +885,9 @@ private void startTopFilmsDownloadTask(int pageNumber) /** * Метод показа ТОП-100 популярных фильмов - асинхронно загружает список фильмов из Сети * - * @param ifBeginFromPageOne если True, текущий список очищается и показ начинается с первой страницы + * @param isBeginFromPageOne если True, текущий список очищается и показ начинается с первой страницы */ - private void switchUIToPopularFilmsAsync(boolean ifBeginFromPageOne, boolean isDownloadNew) + private void switchUIToPopularFilmsAsync(boolean isBeginFromPageOne, boolean isDownloadNew) { customToolBar.setTitle(R.string.action_popular_title); if (_currentViewMode == ViewMode.POPULAR && !isDownloadNew) @@ -841,21 +897,21 @@ private void switchUIToPopularFilmsAsync(boolean ifBeginFromPageOne, boolean isD _currentViewMode = ViewMode.POPULAR; lastListViewPos2 = scroller.getScrollY(); - if (ifBeginFromPageOne && isDownloadNew) + if (isBeginFromPageOne && isDownloadNew) { clearList(false); } else if (!isDownloadNew) { - clearList(!ifBeginFromPageOne); + clearList(!isBeginFromPageOne); } - if (ifBeginFromPageOne) + if (isBeginFromPageOne) { - _currentPageNumber = 1; + _nextPageNumber = 1; } if (isDownloadNew) { - this.startTopFilmsDownloadTask(_currentPageNumber); + this.startTopFilmsDownloadTask(_nextPageNumber); } else { diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index b42d047..d7acd7a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,118 +1,133 @@ - + tools:context=".MainActivity" + > - + - + + - + - + - + app:cardBackgroundColor="?colorOnPrimary" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/top_toolbar"> - - tools:ignore="VisualLintTextFieldSize,TextContrastCheck" /> - - + + + - - - + + - - - - - + android:paddingLeft="5dp" + android:paddingRight="5dp" + android:scrollbarStyle="outsideOverlay" + android:scrollbarThumbVertical="@color/neo_light" + tools:ignore="SpeakableTextPresentCheck"> - - + + + + + - \ No newline at end of file + + From 2ffb1e1f0d3a8061fe10b573714f266bcb636e92 Mon Sep 17 00:00:00 2001 From: Mikhail M Date: Fri, 24 Feb 2023 16:33:35 +0300 Subject: [PATCH 09/13] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BF=D0=BE=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BF=D0=BE=D0=BB=D1=8F=20=D1=84=D0=B8=D0=BB=D1=8C?= =?UTF-8?q?=D1=82=D1=80=D0=B0=20=D0=BF=D1=80=D0=B8=20=D0=BF=D0=BE=D0=B2?= =?UTF-8?q?=D0=BE=D1=80=D0=BE=D1=82=D0=B5=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/mmu/tinkoffkinolab/MainActivity.java | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java index fe50cd5..2348126 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java @@ -312,7 +312,7 @@ protected void onCreate(Bundle savedInstanceState) this.isLandscape = this.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; this.setEventHandlers(); - + if (_cardList.isEmpty()) { switchUIToPopularFilmsAsync(true, true); @@ -329,10 +329,6 @@ protected void onStart() { super.onStart(); Log.d(LOG_TAG, "---------------- In the onStart() method"); - if (this.isLandscape) - { - onScreenRotate(); - } Log.w(LOG_TAG, "Exit of onStart() method ---------------------"); } @@ -355,11 +351,16 @@ protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) } else if (_currentViewMode == ViewMode.POPULAR) { - switchUIToPopularFilmsAsync(false,false); + switchUIToPopularFilmsAsync(false, false); sourceList = _cardList; } fillCardListUIFrom((_nextPageNumber - 2) * WebDataDownloadTask.FILMS_COUNT_PER_PAGE, Objects.requireNonNull(sourceList)); + final String query = Objects.requireNonNull(txtQuery.getText()).toString(); + if (!query.isBlank()) + { + filterCardListUI(query, this.getCurrentFilmListStream()); + } Log.d(LOG_TAG, "---------------------- состояние восстановлено! ----------------"); } @@ -397,6 +398,16 @@ else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) onScreenRotate(); } + @Override + protected void onResume() + { + super.onResume(); + if (this.isLandscape) + { + onScreenRotate(); + } + } + /** * Обработчик сохранения состояния программы - если список Избранного пуст, то удаляет кеш изображений * @@ -567,9 +578,7 @@ public void afterTextChanged(Editable s) } else { - filterFilmCardsUI(s.toString(), _currentViewMode == ViewMode.FAVOURITES ? - getFavouritesMap().values().stream() : _currentViewMode == ViewMode.POPULAR ? - _cardList.stream() : Stream.empty()); + filterCardListUI(s.toString(), getCurrentFilmListStream()); } } }; @@ -577,6 +586,16 @@ public void afterTextChanged(Editable s) return searchTextChangeWatcher; } + /** + * @return Возвращает нужный поток Фильмов в зависимости от выбранной вкладки UI + */ + private Stream> getCurrentFilmListStream() + { + return _currentViewMode == ViewMode.FAVOURITES ? + getFavouritesMap().values().stream() : _currentViewMode == ViewMode.POPULAR ? + _cardList.stream() : Stream.empty(); + } + /** * Обработчик клика на элемент списка (карточку Фильма) - открывает окно с подробным описанием Фильма. * @@ -638,7 +657,7 @@ private void setEventHandlers() /** * Метод показа (вывода из невидимости) всех карточек фильмов * - * @apiNote Вызов имеет смысл, только если перед этим был вызов {@link #filterFilmCardsUI(String, Stream)} )} + * @apiNote Вызов имеет смысл, только если перед этим был вызов {@link #filterCardListUI(String, Stream)} )} * @implNote Сбрасывает признак фильтрации списка {@link #isFiltered} */ private void showFilmCardsUI() @@ -660,7 +679,7 @@ private void showFilmCardsUI() * @apiNote Метод не выдаёт совпадения для слов короче 3-х символов (для исключения предлогов) * @implSpec При пустом query не делает НИЧЕГО - для отмены фильтра используйте {@link #showFilmCardsUI()} */ - private void filterFilmCardsUI(String query, Stream> cardStream) + private void filterCardListUI(String query, Stream> cardStream) { if (query.isBlank()) { @@ -924,7 +943,7 @@ else if (!isDownloadNew) } else { - filterFilmCardsUI(query, _cardList.stream()); + filterCardListUI(query, _cardList.stream()); } } } @@ -956,7 +975,7 @@ private void switchUIToFavouriteFilms(boolean forceRefresh) final var query = Objects.requireNonNull(txtQuery.getText()).toString(); if (!query.isBlank()) { - filterFilmCardsUI(query, getFavouritesMap().values().stream()); + filterCardListUI(query, getFavouritesMap().values().stream()); } else if (!forceRefresh) { From 815923a3d6eb3edb248ea1ede9a607ca498d5a42 Mon Sep 17 00:00:00 2001 From: Mikhail M Date: Fri, 24 Feb 2023 16:59:57 +0300 Subject: [PATCH 10/13] =?UTF-8?q?=D0=A0=D0=B5=D1=84=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D0=BD=D0=B3=20-=20=D1=81=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=BE=D0=BA=20=D0=B8=D0=B7=D0=B1=D1=80=D0=B0=D0=BD=D0=BD=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D1=82=D0=B5=D0=BF=D0=B5=D1=80=D1=8C=20=D1=82?= =?UTF-8?q?=D0=BE=D0=B6=D0=B5=20=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=87=D0=B5?= =?UTF-8?q?=D1=81=D0=BA=D0=B8=D0=B9,=20=D0=BA=D0=B0=D0=BA=20=D0=B8=20?= =?UTF-8?q?=D0=A1=D0=BF=D0=B8=D1=81=D0=BE=D0=BA=20=D0=9F=D0=BE=D0=BF=D1=83?= =?UTF-8?q?=D0=BB=D1=8F=D1=80=D0=BD=D1=8B=D1=85=20=D1=84=D0=B8=D0=BB=D1=8C?= =?UTF-8?q?=D0=BC=D0=BE=D0=B2=20-=20=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7?= =?UTF-8?q?=D0=BA=D0=B0=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0=20=D0=B8?= =?UTF-8?q?=D0=B7=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0=20=D0=B8=D0=B4=D1=91?= =?UTF-8?q?=D1=82=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=B5=D1=81?= =?UTF-8?q?=D0=BB=D0=B8=20=D0=BD=D0=B5=D1=82=20=D1=81=D0=BE=D1=85=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D1=91=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=81=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=BA=D0=B0=20=D0=B2=20=D0=BF=D0=B0=D0=BC=D1=8F?= =?UTF-8?q?=D1=82=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/mmu/tinkoffkinolab/MainActivity.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java index 2348126..0ddd8f0 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java @@ -47,6 +47,7 @@ public class MainActivity extends AppCompatActivity { + private boolean _isFinished; //region 'Типы' @@ -223,7 +224,7 @@ private int downloadPopularFilmList(int pageNumber) //region 'Свойства' - private Map> favouritesMap; + private static Map> favouritesMap; /** * Возвращает список избранных фильмов @@ -283,7 +284,7 @@ protected void onCreate(Bundle savedInstanceState) this.setSupportActionBar(customToolBar); // восстанавливаем список избранного из файла this.favouritesListFilePath = new File(this.getFilesDir(), FAVOURITES_LIST_FILE_NAME); - if (this.favouritesListFilePath.exists()) + if (getFavouritesMap().isEmpty() && this.favouritesListFilePath.exists()) { loadFavouritesList(); Log.d(LOG_TAG, "Список избранного загружен из файла:\n " + this.favouritesListFilePath); @@ -408,6 +409,13 @@ protected void onResume() } } + @Override + protected void onDestroy() + { + super.onDestroy(); + _isFinished = isFinishing(); + } + /** * Обработчик сохранения состояния программы - если список Избранного пуст, то удаляет кеш изображений * @@ -936,7 +944,7 @@ else if (!isDownloadNew) { fillCardListUIFrom(0, _cardList); final var query = Objects.requireNonNull(txtQuery.getText()).toString(); - if (query.isBlank()) + if (query.isBlank() && lastListViewPos > 0) { // на основе кода отсюда: https://stackoverflow.com/a/3263540/2323972 scroller.post(() -> scroller.scrollTo(0, lastListViewPos)); From be71ba268152a920bdeb5c0aacba3ca3b199b49d Mon Sep 17 00:00:00 2001 From: Mikhail M Date: Mon, 27 Feb 2023 14:05:36 +0300 Subject: [PATCH 11/13] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B8=D0=BB=D1=81?= =?UTF-8?q?=D1=8F=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D1=80=D0=B0=D0=B7=D0=BC=D0=B5=D1=82=D0=BA=D0=B8=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=20=D0=BF=D0=BE=D0=B2=D0=BE=D1=80=D0=BE=D1=82=D0=B5?= =?UTF-8?q?=20=D0=B2=20=D0=B3=D0=BE=D1=80=D0=B8=D0=B7=D0=BE=D0=BD=D1=82?= =?UTF-8?q?=D0=B0=D0=BB=D1=8C=D0=BD=D1=83=D1=8E=20=D0=BE=D1=80=D0=B8=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D1=8E=20-=20=D0=BA=D0=BE=D0=BC?= =?UTF-8?q?=D0=BC=D0=B8=D1=82=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=BD=D0=BE=D1=81=D0=BE=D0=BC=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=B4=D0=B0=20=D1=83=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=84=D1=80=D0=B0=D0=B3=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=BE=D0=BC=20=D0=B8=D0=B7=20CardActivity=20=D0=B2=20=D1=81?= =?UTF-8?q?=D0=B0=D0=BC=20=D1=84=D1=80=D0=B0=D0=B3=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/mmu/tinkoffkinolab/MainActivity.java | 145 +++++++++++------- app/src/main/res/layout/activity_main.xml | 5 +- 2 files changed, 92 insertions(+), 58 deletions(-) diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java index 0ddd8f0..86807e1 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java @@ -6,6 +6,8 @@ import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.cardview.widget.CardView; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentContainerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.annotation.SuppressLint; @@ -24,6 +26,7 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; +import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.widget.ImageView; import android.widget.LinearLayout; @@ -47,7 +50,8 @@ public class MainActivity extends AppCompatActivity { - private boolean _isFinished; + private Fragment detailsFragment; + private LinearLayout horizontalContainer; //region 'Типы' @@ -259,6 +263,47 @@ public AlertDialog getConfirmClearDialog() return confirmClearDialog; } + + private TextWatcher searchTextChangeWatcher; + + /** + * Обработчик изменения текста в виджете Поиска + */ + @NonNull + private TextWatcher getSearchTextChangeWatcher() + { + if (this.searchTextChangeWatcher == null) + { + this.searchTextChangeWatcher = new TextWatcher() + { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) + { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) + { + } + + @Override + public void afterTextChanged(Editable s) + { + final var query = s.toString(); + if (query.isBlank()) + { + showFilmCardsUI(); + } + else + { + filterCardListUI(s.toString(), getCurrentFilmListStream()); + } + } + }; + } + return searchTextChangeWatcher; + } + //endregion 'Свойства' @@ -311,6 +356,7 @@ protected void onCreate(Bundle savedInstanceState) this.imageCacheDirPath = new File(this.getCacheDir(), Constants.FAVOURITES_CASH_DIR_NAME); this.scroller = findViewById(R.id.card_scroller); this.isLandscape = this.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; + this.horizontalContainer = findViewById(R.id.horizontal_view); this.setEventHandlers(); @@ -330,6 +376,7 @@ protected void onStart() { super.onStart(); Log.d(LOG_TAG, "---------------- In the onStart() method"); + this.horizontalContainer.setWeightSum(1); Log.w(LOG_TAG, "Exit of onStart() method ---------------------"); } @@ -365,12 +412,6 @@ else if (_currentViewMode == ViewMode.POPULAR) Log.d(LOG_TAG, "---------------------- состояние восстановлено! ----------------"); } - private void onScreenRotate() - { - final var isBlank = Objects.requireNonNull(txtQuery.getText()).toString().isBlank(); - this.inputPanel.setVisibility(isLandscape && isBlank ? View.GONE : View.VISIBLE); - } - /** * Обработчик поворота экрана * @@ -403,17 +444,24 @@ else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) protected void onResume() { super.onResume(); - if (this.isLandscape) + //if (this.isLandscape) { onScreenRotate(); } } - @Override - protected void onDestroy() + private void onScreenRotate() { - super.onDestroy(); - _isFinished = isFinishing(); + final var isBlank = Objects.requireNonNull(txtQuery.getText()).toString().isBlank(); + this.inputPanel.setVisibility(this.isLandscape && isBlank ? View.GONE : View.VISIBLE); + + if (!this.isLandscape && this.detailsFragment != null) + { + //hl.setWeightSum(this.isLandscape ? 2 : 1); + this.horizontalContainer.setWeightSum(1); + getSupportFragmentManager().beginTransaction().remove(detailsFragment).commit(); + this.detailsFragment = null; + } } /** @@ -426,7 +474,9 @@ protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); // TODO: Нужно как-то определять, когда происходит выход из программы. а не поворот, чтобы - // не перезаписывать файл при каждом повороте + // не перезаписывать файл при каждом повороте - если же это невозможно, то лучше наверно будет + // обновлять файл избранного не при переключении Экранов, а при изменении списка Избранного, + // т.к. по идее это должно происходить реже. if (favouritesMap == null || favouritesMap.isEmpty()) { this.deleteFile(Constants.FAVOURITES_LIST_FILE_NAME); @@ -475,6 +525,8 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) } case R.id.action_go_to_top: { + // N.B. этот код достаточно надёжен, т.к. внутри SwipeRefreshContainer может быть только + // что-то типа ScrollView swipeRefreshContainer.getChildAt(0).scrollTo(0, 0); break; } @@ -533,9 +585,9 @@ private View.OnClickListener getOnLikeButtonClickListener(Map ca /** * Обработчик прокрутки списка фильмов до конца - подгружает новую "страницу" в конец списка топов */ - private void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) + private void onScrollChange(@NonNull View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { - boolean isBottomReached = cardsContainer.getBottom() - v.getBottom() - scrollY == 0; + final boolean isBottomReached = cardsContainer.getBottom() - v.getBottom() - scrollY == 0; if (isBottomReached && !isFiltered) { if (_currentViewMode == ViewMode.POPULAR && _nextPageNumber < _topFilmsPagesCount) @@ -553,46 +605,7 @@ else if (scrollY == 0) } } - - private TextWatcher searchTextChangeWatcher; - - /** - * Обработчик изменения текста в виджете Поиска - */ - @NonNull - private TextWatcher getSearchTextChangeWatcher() - { - if (this.searchTextChangeWatcher == null) - { - searchTextChangeWatcher = new TextWatcher() - { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) - { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) - { - } - - @Override - public void afterTextChanged(Editable s) - { - final var query = s.toString(); - if (query.isBlank()) - { - showFilmCardsUI(); - } - else - { - filterCardListUI(s.toString(), getCurrentFilmListStream()); - } - } - }; - } - return searchTextChangeWatcher; - } + /** * @return Возвращает нужный поток Фильмов в зависимости от выбранной вкладки UI @@ -613,9 +626,29 @@ private Stream> getCurrentFilmListStream() @NonNull private View.OnClickListener getOnListItemClickListener(String id, String title) { - return (View v) -> showFilmCardActivity(id, title); + return (View v) -> { + if (this.isLandscape) + { + //final FragmentContainerView detailsContainer = findViewById(R.id.details_view); + if (this.detailsFragment == null) + { + this.detailsFragment = new Fragment(R.layout.fragment_card); + getSupportFragmentManager().beginTransaction().add(R.id.details_view, + this.detailsFragment).commit(); + } + getSupportFragmentManager().beginTransaction().show(this.detailsFragment).commit(); + horizontalContainer.setWeightSum(2); + } + else + { + showFilmCardActivity(id, title); + } + }; } + /** + * Обработчик скрытия панели Поиска - очищает поле ввода поискового запроса + */ private void onSearchBarVisibleChanged() { if (this.inputPanel.getVisibility() == View.GONE) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index d7acd7a..621c68f 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -98,7 +98,7 @@ @@ -123,9 +123,10 @@ From 4be219e4a05f4b9d3c92cf8dbfc55546518c5099 Mon Sep 17 00:00:00 2001 From: Mikhail M Date: Mon, 27 Feb 2023 16:28:10 +0300 Subject: [PATCH 12/13] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D1=8C=20"=D1=81=D0=BC=D0=B0=D1=85=D0=BD=D1=83=D1=82?= =?UTF-8?q?=D1=8C"=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BE=D0=B1=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B5=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8=20=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B8=D0=B7=20=D0=A1=D0=B5=D1=82?= =?UTF-8?q?=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/mmu/tinkoffkinolab/CardActivity.java | 6 +- .../org/mmu/tinkoffkinolab/MainActivity.java | 8 +- app/src/main/res/layout/activity_card.xml | 1 - app/src/main/res/layout/activity_main.xml | 1 + app/src/main/res/layout/fragment_card.xml | 95 ++++++++++--------- 5 files changed, 55 insertions(+), 56 deletions(-) diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java b/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java index 01669e7..99bbaae 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java @@ -47,7 +47,6 @@ public class CardActivity extends AppCompatActivity private WebDataDownloadTask downloadTask; private TextView txtHeader; private TextView txtContent; - private View androidContentView; private View progBar; private boolean _isHorizontal; @@ -174,7 +173,6 @@ public void onFragmentViewCreated(@NonNull FragmentManager fm, @NonNull Fragment imgPoster = v.findViewById(R.id.poster_image_view); txtHeader = v.findViewById(R.id.card_title); txtContent = v.findViewById(R.id.card_content); - androidContentView = v.findViewById(android.R.id.content); super.onFragmentViewCreated(fm, f, v, savedInstanceState); getFilmDataAsync(); } @@ -189,12 +187,10 @@ public void onConfigurationChanged(@NonNull Configuration newConfig) if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { _isHorizontal = true; - //Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show(); } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { _isHorizontal = false; - //Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show(); } } @@ -270,7 +266,7 @@ public void onError(Exception e) */ private void showSnackBar(String message) { - var popup = Snackbar.make(androidContentView, message, Snackbar.LENGTH_INDEFINITE); + var popup = Snackbar.make(this.imgPoster, message, Snackbar.LENGTH_INDEFINITE); popup.setAction(R.string.repeat_button_caption, view -> { getFilmDataAsync(); popup.dismiss(); diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java index 86807e1..dc5203d 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java @@ -7,7 +7,6 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.cardview.widget.CardView; import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentContainerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.annotation.SuppressLint; @@ -26,7 +25,6 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.widget.ImageView; import android.widget.LinearLayout; @@ -213,7 +211,7 @@ private int downloadPopularFilmList(int pageNumber) */ private AsyncTask downloadTask; private TextInputEditText txtQuery; - private View androidContentView; + private View coordinatorView; private LinearLayout cardsContainer; private LayoutInflater layoutInflater; private boolean isFiltered; @@ -344,7 +342,7 @@ protected void onCreate(Bundle savedInstanceState) this.isRus = Locale.getDefault().getLanguage().equalsIgnoreCase("ru"); this.layoutInflater = getLayoutInflater(); - this.androidContentView = findViewById(android.R.id.content); + this.coordinatorView = findViewById(R.id.root_container); this.progBar = findViewById(R.id.progress_bar); this.progBar.setVisibility(View.GONE); this.txtQuery = findViewById(R.id.txt_input); @@ -910,7 +908,7 @@ private boolean addToOrRemoveFromFavourites(String id, Map cardD */ private Snackbar showErrorSnackBar(String message) { - var popup = Snackbar.make(this.androidContentView, message, Snackbar.LENGTH_INDEFINITE); + var popup = Snackbar.make(coordinatorView, message, Snackbar.LENGTH_INDEFINITE); popup.setAction(R.string.repeat_button_caption, view -> { var text = Objects.requireNonNull(txtQuery.getText()).toString().replace("null", ""); this.startTopFilmsDownloadTask(_nextPageNumber); diff --git a/app/src/main/res/layout/activity_card.xml b/app/src/main/res/layout/activity_card.xml index 7c6df63..cbb3120 100644 --- a/app/src/main/res/layout/activity_card.xml +++ b/app/src/main/res/layout/activity_card.xml @@ -1,5 +1,4 @@ - - - - - - - - + + android:scaleType="centerCrop" + android:contentDescription="@string/filmposter_image_alt_text" + tools:srcCompat="@tools:sample/backgrounds/scenic" /> - + android:orientation="vertical" + > + + + + - - \ No newline at end of file + + \ No newline at end of file From 2c2b696fe3e3339c18d1fd6a9f8bc0f8bb8b6785 Mon Sep 17 00:00:00 2001 From: Mikhail M Date: Mon, 27 Feb 2023 18:34:31 +0300 Subject: [PATCH 13/13] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BB=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=83?= =?UTF-8?q?=20=D1=81=20=D1=84=D1=80=D0=B0=D0=B3=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0=D0=BC=D0=B8=20=D0=BF=D1=80=D0=B8=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D1=85=D0=BE=D0=B4=D0=B5=20=D0=B2=20=D0=B3=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D0=B7=D0=BE=D0=BD=D1=82=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20=D1=80=D0=B5=D0=B6=D0=B8=D0=BC=20=D0=B8=20=D0=BE=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D1=82=D0=BD=D0=BE.=20=D0=97=D0=B0=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE=D0=BB=D0=BD=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D1=8C=D1=8E=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=BE.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/mmu/tinkoffkinolab/CardActivity.java | 205 ++----------- .../org/mmu/tinkoffkinolab/CardFragment.java | 285 ++++++++++++++++-- .../org/mmu/tinkoffkinolab/MainActivity.java | 51 ++-- app/src/main/res/layout/activity_card.xml | 16 - app/src/main/res/layout/activity_main.xml | 3 +- app/src/main/res/layout/fragment_card.xml | 37 ++- 6 files changed, 325 insertions(+), 272 deletions(-) diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java b/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java index 99bbaae..5a40a59 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/CardActivity.java @@ -1,5 +1,7 @@ package org.mmu.tinkoffkinolab; +import static org.mmu.tinkoffkinolab.Constants.ADAPTER_FILM_ID; +import static org.mmu.tinkoffkinolab.Constants.ADAPTER_TITLE; import static org.mmu.tinkoffkinolab.Constants.LOG_TAG; import android.content.res.Configuration; @@ -39,112 +41,9 @@ public class CardActivity extends AppCompatActivity { - //region 'Поля и константы' - private static final Map _cardData = new HashMap<>(); - private ImageView imgPoster; private String filmId; - private WebDataDownloadTask downloadTask; - private TextView txtHeader; - private TextView txtContent; - private View progBar; - private boolean _isHorizontal; - - //endregion 'Поля и константы' - - - - //region 'Типы' - - private class WebDataDownloadTask extends AsyncTask - { - private final FilmsApi filmsApi; - private Map.Entry error; - - public Map.Entry getError() - { - return error; - } - - public WebDataDownloadTask(FilmsApi engine) - { - filmsApi = engine; - } - - @Override - protected void onPreExecute() - { - super.onPreExecute(); - progBar.setVisibility(View.VISIBLE); - Log.d(LOG_TAG, "Начало загрузки веб-ресурса..."); - } - - @Override - protected Void doInBackground(Void... unused) - { - try - { - final var response = filmsApi.apiV22FilmsIdGet(Integer.parseInt(filmId)); - final var engName = Objects.requireNonNullElse(response.getNameEn(), ""); - _cardData.put(Constants.ADAPTER_TITLE, response.getNameRu() + (!engName.isBlank() ? - System.lineSeparator() + "[" + engName +"]" : "")); - final var genres = "\n\n Жанры: " + response.getGenres().stream() - .map(g -> g.getGenre() + ", ") - .collect(Collectors.joining()) - .replaceFirst(",\\s*$", ""); - final var countries = "\n\n Страны: " + response.getCountries().stream() - .map(country -> country.getCountry() + ", ") - .collect(Collectors.joining()).replaceFirst(",\\s*$", ""); - final var res = Objects.requireNonNullElse(response.getDescription(), "") + - genres + countries; - _cardData.put(Constants.ADAPTER_CONTENT, res); - _cardData.put(Constants.ADAPTER_POSTER_PREVIEW_URL, response.getPosterUrl()); - } - catch (RuntimeException ex) - { - var mes = Objects.requireNonNullElse(ex.getMessage(), ""); - error = new AbstractMap.SimpleEntry<>(ex, mes); - if (ex instanceof ApiException) - { - final var apiEx = (ApiException)ex; - final var headers = apiEx.getResponseHeaders(); - final var headersText = headers == null ? "" : headers.entrySet().stream() - .map(entry -> entry.getKey() + ": " + String.join(" \n", entry.getValue())) - .collect(Collectors.joining()); - mes += String.format(Locale.ROOT, " %s (ErrorCode: %d), ResponseHeaders: \n%s\n ResponseBody: \n%s\n", - Constants.KINO_API_ERROR_MES, apiEx.getCode(), headersText, apiEx.getResponseBody()); - } - Log.e(LOG_TAG, mes.isEmpty() ? Constants.UNKNOWN_WEB_ERROR_MES : mes, ex); - } - return null; - } - - /** - * @apiNote Этот метод выполняется в потоке интерфейса - * - * @param unused The result of the operation computed by {@link #doInBackground}. - */ - @Override - protected void onPostExecute(Void unused) - { - super.onPostExecute(unused); - progBar.setVisibility(View.GONE); - if (downloadTask.getError() != null) - { - final var mes = downloadTask.getError().getValue(); - showSnackBar(Constants.UNKNOWN_WEB_ERROR_MES); - } - else - { - Log.d(LOG_TAG, "Загрузка Веб-ресурса завершена успешно"); - fillCardUI(); - } - } - } - - //endregion 'Типы' - - + //region 'Обработчики' @@ -160,38 +59,35 @@ protected void onCreate(Bundle savedInstanceState) this.setSupportActionBar(customToolBar); Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayShowHomeEnabled(true); - progBar = findViewById(R.id.progress_bar); filmId = getIntent().getStringExtra(Constants.ADAPTER_FILM_ID); getSupportFragmentManager().registerFragmentLifecycleCallbacks(new FragmentManager.FragmentLifecycleCallbacks() { + private CardFragment cardFragment; @Override public void onFragmentViewCreated(@NonNull FragmentManager fm, @NonNull Fragment f, @NonNull View v, @Nullable Bundle savedInstanceState) { - imgPoster = v.findViewById(R.id.poster_image_view); - txtHeader = v.findViewById(R.id.card_title); - txtContent = v.findViewById(R.id.card_content); super.onFragmentViewCreated(fm, f, v, savedInstanceState); - getFilmDataAsync(); + cardFragment = ((CardFragment)f); + cardFragment.setDataLoadListener(() -> onDataLoaded_Handler()); + cardFragment.getFilmDataAsync(filmId, customToolBar.getTitle().toString()); + } + + @Override + public void onFragmentDestroyed(@NonNull FragmentManager fm, @NonNull Fragment f) + { + super.onFragmentDestroyed(fm, f); + cardFragment.removeDataLoadListener(); } }, false); + } - - @Override - public void onConfigurationChanged(@NonNull Configuration newConfig) + public void onDataLoaded_Handler() { - super.onConfigurationChanged(newConfig); - if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) - { - _isHorizontal = true; - } - else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) - { - _isHorizontal = false; - } + Objects.requireNonNull(getSupportActionBar()).setDisplayShowTitleEnabled(false); } /** @@ -218,71 +114,6 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) - //region 'Методы' - - /** - * Метод заполнения UI карточки фильма из скачанных данных - */ - private void fillCardUI() - { - progBar.setVisibility(View.VISIBLE); - // N.B.: параметры fit() и centerCrop() могут сильно замедлять загрузку! - final var posterUrl = _cardData.get(Constants.ADAPTER_POSTER_PREVIEW_URL); - Picasso.get().load(posterUrl).into(imgPoster, new Callback() - { - @Override - public void onSuccess() - { - progBar.setVisibility(View.GONE); - Objects.requireNonNull(getSupportActionBar()).setDisplayShowTitleEnabled(false); - //noinspection ConstantConditions - txtHeader.setText(Objects.requireNonNullElse(_cardData.get(Constants.ADAPTER_TITLE), - getSupportActionBar().getTitle())); - final var textContent = _cardData.get(Constants.ADAPTER_CONTENT); - txtContent.setText(Objects.requireNonNullElse(textContent, "")); - imgPoster.setOnClickListener(v1 -> { - Utils.showFullScreenPhoto(Uri.parse(posterUrl), v1.getContext()); - }); - final ScrollView sv = findViewById(R.id.fragment_scroll); - sv.postDelayed(() -> { - if (!Utils.isViewOnScreen(txtContent)) - { - sv.smoothScrollTo(0, imgPoster.getHeight() / 2); - } - }, 500); - } - - @Override - public void onError(Exception e) - { - progBar.setVisibility(View.GONE); - Log.e(LOG_TAG, "Ошибка загрузки большого постера", e); - } - }); - } - - /** - * Метод отображения всплывющей подсказки - */ - private void showSnackBar(String message) - { - var popup = Snackbar.make(this.imgPoster, message, Snackbar.LENGTH_INDEFINITE); - popup.setAction(R.string.repeat_button_caption, view -> { - getFilmDataAsync(); - popup.dismiss(); - }); - popup.show(); - } - - private void getFilmDataAsync() - { - if (downloadTask != null && !downloadTask.isCancelled() && (downloadTask.getStatus() == AsyncTask.Status.RUNNING)) - { - downloadTask.cancel(true); - } - downloadTask = (WebDataDownloadTask)new WebDataDownloadTask(FilmsApiHelper.getFilmsApi()).execute(); - } - - //endregion 'Методы' + } \ No newline at end of file diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/CardFragment.java b/app/src/main/java/org/mmu/tinkoffkinolab/CardFragment.java index 9ee4d4d..1f24741 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/CardFragment.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/CardFragment.java @@ -1,70 +1,289 @@ package org.mmu.tinkoffkinolab; +import static org.mmu.tinkoffkinolab.Constants.LOG_TAG; + +import android.net.Uri; +import android.os.AsyncTask; import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ScrollView; +import android.widget.TextView; + +import com.google.android.material.snackbar.Snackbar; +import com.squareup.picasso.Callback; +import com.squareup.picasso.Picasso; + +import java.util.AbstractMap; +import java.util.EventListener; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import io.swagger.client.ApiException; +import io.swagger.client.api.FilmsApi; /** * A simple {@link Fragment} subclass. - * Use the {@link CardFragment#newInstance} factory method to * create an instance of this fragment. */ public class CardFragment extends Fragment { + + + //region 'Типы' + + private class WebDataDownloadTask extends AsyncTask + { + private final FilmsApi filmsApi; + private Map.Entry error; + + public Map.Entry getError() + { + return error; + } + + public WebDataDownloadTask(FilmsApi engine) + { + filmsApi = engine; + } + + @Override + protected void onPreExecute() + { + super.onPreExecute(); + progBar.setVisibility(View.VISIBLE); + Log.d(LOG_TAG, "Начало загрузки веб-ресурса..."); + } + + @Override + protected Void doInBackground(String... filmId) + { + try + { + final var response = filmsApi.apiV22FilmsIdGet(Integer.parseInt(filmId[0])); + final var engName = Objects.requireNonNullElse(response.getNameEn(), ""); + _cardData.put(Constants.ADAPTER_TITLE, response.getNameRu() + (!engName.isBlank() ? + System.lineSeparator() + "[" + engName + "]" : "")); + final var genres = "\n\n Жанры: " + response.getGenres().stream() + .map(g -> g.getGenre() + ", ") + .collect(Collectors.joining()) + .replaceFirst(",\\s*$", ""); + final var countries = "\n\n Страны: " + response.getCountries().stream() + .map(country -> country.getCountry() + ", ") + .collect(Collectors.joining()).replaceFirst(",\\s*$", ""); + final var res = Objects.requireNonNullElse(response.getDescription(), "") + + genres + countries; + _cardData.put(Constants.ADAPTER_CONTENT, res); + _cardData.put(Constants.ADAPTER_POSTER_PREVIEW_URL, response.getPosterUrl()); + } + catch (RuntimeException ex) + { + var mes = Objects.requireNonNullElse(ex.getMessage(), ""); + error = new AbstractMap.SimpleEntry<>(ex, mes); + if (ex instanceof ApiException) + { + final var apiEx = (ApiException) ex; + final var headers = apiEx.getResponseHeaders(); + final var headersText = headers == null ? "" : headers.entrySet().stream() + .map(entry -> entry.getKey() + ": " + String.join(" \n", entry.getValue())) + .collect(Collectors.joining()); + mes += String.format(Locale.ROOT, " %s (ErrorCode: %d), ResponseHeaders: \n%s\n ResponseBody: \n%s\n", + Constants.KINO_API_ERROR_MES, apiEx.getCode(), headersText, apiEx.getResponseBody()); + } + Log.e(LOG_TAG, mes.isEmpty() ? Constants.UNKNOWN_WEB_ERROR_MES : mes, ex); + } + return null; + } + + /** + * @param unused The result of the operation computed by {@link #doInBackground}. + * @apiNote Этот метод выполняется в потоке интерфейса + */ + @Override + protected void onPostExecute(Void unused) + { + super.onPostExecute(unused); + progBar.setVisibility(View.GONE); + if (downloadTask.getError() != null) + { + final var mes = downloadTask.getError().getValue(); + showSnackBar(Constants.UNKNOWN_WEB_ERROR_MES); + } + else + { + Log.d(LOG_TAG, "Загрузка Веб-ресурса завершена успешно"); + fillCardUI(); + } + } + } + + //endregion 'Типы' + + + //region 'Поля и константы' + + private static final Map _cardData = new HashMap<>(); + private ImageView imgPoster; + + private WebDataDownloadTask downloadTask; + private TextView txtHeader; + private TextView txtContent; + private View progBar; + + private String filmId_param; + private String filmTitle_param; + + //endregion 'Поля и константы' + + + //region 'События' + + interface DataLoadSuccessListener extends EventListener + { + void onFilmDataLoaded(); + } + + public DataLoadSuccessListener getDataLoadListener() + { + return dataLoadListener; + } + + public void setDataLoadListener(DataLoadSuccessListener dataLoadListener) + { + this.dataLoadListener = dataLoadListener; + } + + public void removeDataLoadListener() + { + this.dataLoadListener = null; + } - // TODO: Rename parameter arguments, choose names that match - // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER - private static final String ARG_PARAM1 = "param1"; - private static final String ARG_PARAM2 = "param2"; + private DataLoadSuccessListener dataLoadListener; + + //endregion 'События' - // TODO: Rename and change types of parameters - private String mParam1; - private String mParam2; // Required empty public constructor public CardFragment() { } + + + //region 'Обработчики' + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + { + return inflater.inflate(R.layout.fragment_card, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) + { + super.onViewCreated(view, savedInstanceState); + imgPoster = view.findViewById(R.id.poster_image_view); + txtHeader = view.findViewById(R.id.card_title); + txtContent = view.findViewById(R.id.card_content); + progBar = view.findViewById(R.id.progress_bar); + } + + //endregion 'Обработчики' + + + //region 'Методы' + /** - * Use this factory method to create a new instance of - * this fragment using the provided parameters. - * - * @param param1 Parameter 1. - * @param param2 Parameter 2. - * @return A new instance of fragment CardFragment. + * Метод заполнения UI карточки фильма из скачанных данных */ - // TODO: Rename and change types and number of parameters - public static CardFragment newInstance(String param1, String param2) + private void fillCardUI() { - CardFragment fragment = new CardFragment(); - Bundle args = new Bundle(); - args.putString(ARG_PARAM1, param1); - args.putString(ARG_PARAM2, param2); - fragment.setArguments(args); - return fragment; + progBar.setVisibility(View.VISIBLE); + // N.B.: параметры fit() и centerCrop() могут сильно замедлять загрузку! + final var posterUrl = _cardData.get(Constants.ADAPTER_POSTER_PREVIEW_URL); + Picasso.get().load(posterUrl).into(imgPoster, new Callback() + { + @Override + public void onSuccess() + { + progBar.setVisibility(View.GONE); + if (dataLoadListener != null) + { + dataLoadListener.onFilmDataLoaded(); + } + txtHeader.setText(Objects.requireNonNullElse(_cardData.get(Constants.ADAPTER_TITLE), + filmTitle_param)); + final var textContent = _cardData.get(Constants.ADAPTER_CONTENT); + txtContent.setText(Objects.requireNonNullElse(textContent, "")); + imgPoster.setOnClickListener(v1 -> + Utils.showFullScreenPhoto(Uri.parse(posterUrl), v1.getContext()) + ); + final ScrollView sv = requireActivity().findViewById(R.id.fragment_scroll); + sv.postDelayed(() -> { + if (!Utils.isViewOnScreen(txtContent)) + { + sv.smoothScrollTo(0, imgPoster.getHeight() / 2); + } + }, 500); + } + + @Override + public void onError(Exception e) + { + progBar.setVisibility(View.GONE); + Log.e(LOG_TAG, "Ошибка загрузки большого постера", e); + } + }); } - @Override - public void onCreate(Bundle savedInstanceState) + /** + * Метод отображения всплывющей подсказки + */ + private void showSnackBar(String message) { - super.onCreate(savedInstanceState); - if (getArguments() != null) + var popup = Snackbar.make(this.imgPoster, message, Snackbar.LENGTH_INDEFINITE); + popup.setAction(R.string.repeat_button_caption, view -> { + getFilmDataAsync(); + popup.dismiss(); + }); + popup.show(); + } + + private void getFilmDataAsync() + { + getFilmDataAsync(filmId_param, filmTitle_param); + } + + public void getFilmDataAsync(String id, String title) + { + clearView(); + filmId_param = id; + filmTitle_param = title; + if (downloadTask != null && !downloadTask.isCancelled() && (downloadTask.getStatus() == AsyncTask.Status.RUNNING)) { - mParam1 = getArguments().getString(ARG_PARAM1); - mParam2 = getArguments().getString(ARG_PARAM2); + downloadTask.cancel(true); } + downloadTask = (WebDataDownloadTask)new WebDataDownloadTask(FilmsApiHelper.getFilmsApi()).execute(id); } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) + private void clearView() { - // TODO Auto-generated method stub - return inflater.inflate(R.layout.fragment_card, container, false); + imgPoster.setImageDrawable(null); + txtContent.setText(""); + txtHeader.setText(""); } + + //endregion 'Методы' } \ No newline at end of file diff --git a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java index dc5203d..5000ff1 100644 --- a/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java +++ b/app/src/main/java/org/mmu/tinkoffkinolab/MainActivity.java @@ -6,7 +6,7 @@ import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.cardview.widget.CardView; -import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentContainerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.annotation.SuppressLint; @@ -48,8 +48,7 @@ public class MainActivity extends AppCompatActivity { - private Fragment detailsFragment; - private LinearLayout horizontalContainer; + //region 'Типы' @@ -219,6 +218,8 @@ private int downloadPopularFilmList(int pageNumber) private int lastListViewPos2; private View scroller; private boolean isLandscape; + private CardFragment detailsFragment; + private LinearLayout horizontalContainer; //endregion 'Поля и константы' @@ -253,9 +254,9 @@ public AlertDialog getConfirmClearDialog() .setIcon(R.drawable.round_warning_amber_24) .setTitle(R.string.confirm_dialog_title).setMessage(R.string.confirm_clear_dialog_text) .setNegativeButton(android.R.string.no, null) - .setPositiveButton(android.R.string.yes, (dialog, which) -> { - clearList(false); - }) + .setPositiveButton(android.R.string.yes, (dialog, which) -> + clearList(false) + ) .create(); } return confirmClearDialog; @@ -379,7 +380,7 @@ protected void onStart() } /** - * Метод восстановления состояния Активити - Вызывается после {@on + * Метод восстановления состояния Активити - Вызывается после {@link #onStart()} * * @param savedInstanceState the data most recently supplied in {@link #onSaveInstanceState}. * @@ -442,7 +443,6 @@ else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) protected void onResume() { super.onResume(); - //if (this.isLandscape) { onScreenRotate(); } @@ -452,13 +452,15 @@ private void onScreenRotate() { final var isBlank = Objects.requireNonNull(txtQuery.getText()).toString().isBlank(); this.inputPanel.setVisibility(this.isLandscape && isBlank ? View.GONE : View.VISIBLE); - - if (!this.isLandscape && this.detailsFragment != null) + FragmentContainerView fw = findViewById(R.id.details_view); + if (!this.isLandscape) { - //hl.setWeightSum(this.isLandscape ? 2 : 1); + for (var fragment : getSupportFragmentManager().getFragments()) { + if (fragment instanceof CardFragment) { + getSupportFragmentManager().beginTransaction().remove(fragment).commit(); + } + } this.horizontalContainer.setWeightSum(1); - getSupportFragmentManager().beginTransaction().remove(detailsFragment).commit(); - this.detailsFragment = null; } } @@ -627,14 +629,18 @@ private View.OnClickListener getOnListItemClickListener(String id, String title) return (View v) -> { if (this.isLandscape) { - //final FragmentContainerView detailsContainer = findViewById(R.id.details_view); if (this.detailsFragment == null) { - this.detailsFragment = new Fragment(R.layout.fragment_card); - getSupportFragmentManager().beginTransaction().add(R.id.details_view, - this.detailsFragment).commit(); + this.detailsFragment = new CardFragment(); + getSupportFragmentManager().beginTransaction().add(R.id.details_view, this.detailsFragment) + .setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out) + .runOnCommit(() -> + detailsFragment.getFilmDataAsync(id, title)).commit(); + } + else + { + detailsFragment.getFilmDataAsync(id, title); } - getSupportFragmentManager().beginTransaction().show(this.detailsFragment).commit(); horizontalContainer.setWeightSum(2); } else @@ -678,12 +684,8 @@ private void setEventHandlers() this.txtQuery.setOnEditorActionListener((v, actionId, event) -> { // N.B. Похоже, только для действия Done можно реализовать авто скрытие клавиатуры - при // остальных клава остаётся на экране после клика - if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_ACTION_GO || - actionId == EditorInfo.IME_ACTION_SEARCH) - { - return false; - } - return true; + return actionId != EditorInfo.IME_ACTION_DONE && actionId != EditorInfo.IME_ACTION_GO && + actionId != EditorInfo.IME_ACTION_SEARCH; }); // Обработчик изменения текста в поисковом контроле this.txtQuery.addTextChangedListener(getSearchTextChangeWatcher()); @@ -1045,7 +1047,6 @@ else if (_currentViewMode == ViewMode.POPULAR) * @param kinoApiFilmId ИД фильма в API кинопоиска * @param cardTitle Желаемый заголовок карточки */ - @NonNull private void showFilmCardActivity(String kinoApiFilmId, String cardTitle) { final var switchToCardActivityIntent = new Intent(getApplicationContext(), CardActivity.class); diff --git a/app/src/main/res/layout/activity_card.xml b/app/src/main/res/layout/activity_card.xml index cbb3120..198794c 100644 --- a/app/src/main/res/layout/activity_card.xml +++ b/app/src/main/res/layout/activity_card.xml @@ -24,20 +24,4 @@ app:title="@string/app_name" app:titleTextColor="@android:color/white" /> - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 0051912..52efac6 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -128,7 +128,8 @@ android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" - tools:layout="@layout/fragment_card" /> + tools:layout="@layout/fragment_card" + /> diff --git a/app/src/main/res/layout/fragment_card.xml b/app/src/main/res/layout/fragment_card.xml index 0ac5723..3cef52d 100644 --- a/app/src/main/res/layout/fragment_card.xml +++ b/app/src/main/res/layout/fragment_card.xml @@ -2,41 +2,41 @@ + + android:scrollbarStyle="outsideOverlay" + android:scrollbarThumbVertical="@color/neo_light"> + android:orientation="vertical"> + android:orientation="vertical"> + + + + + + + + + \ No newline at end of file