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