15,16,17일차 | 영화 평점 소개 사이트 <- 녹화본으로 흐름 파악할 것
//todo info bottom 안되는 것 해결하기
코드 추가
AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
<application
...
android:screenOrientation="portrait"
android:usesCleartextTraffic="true">
</application>
build.gradle
buildTypes {
...
buildFeatures{
// dataBinding true
viewBinding {
enabled true
}
}
}
def retrofit_version ="2.9.0"
dependencies {
...
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
implementation 'de.hdodenhof:circleimageview:3.1.0'
}
adapter>MovieAdapter.java
package com.example.myapplication.adapter;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.RatingBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.bumptech.glide.load.resource.bitmap.FitCenter;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.example.myapplication.DetailActivity;
import com.example.myapplication.R;
import com.example.myapplication.models.Movie;
import com.example.myapplication.models.YtsData;
import com.example.myapplication.utils.Define;
import java.util.ArrayList;
import java.util.List;
public class MovieAdapter extends RecyclerView.Adapter<MovieAdapter.MyViewHolder> {
private List<Movie> list = new ArrayList<>();
private static final String TAG = MovieAdapter.class.getName();
private Context context;
public MovieAdapter(Context context) {
this.context = context;
}
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View movieItemView = inflater.inflate(R.layout.item_movie_card, parent, false);
return new MyViewHolder(movieItemView); // <---- R.layout.item_movie_card
}
@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
Movie movie = list.get(position);
holder.setItem(movie);
//Todo 옵저버 패턴 만들어서 콜백메서드 처리 후에 디테일 액티비티에서 인텐트를 처리하자
holder.itemView.setOnClickListener(v -> {
Intent intent = new Intent(context, DetailActivity.class);
intent.putExtra(Define.PARAM_MOVIE_OBJ, movie);
context.startActivity(intent);
});
}
@Override
public int getItemCount() {
return list.size();
}
// 내부 클래스
public static class MyViewHolder extends RecyclerView.ViewHolder {
View itemView;
ImageView posterIv;
TextView titleTv;
TextView ratingTv;
RatingBar ratingBar;
public MyViewHolder(@NonNull View itemView) {
super(itemView);
this.itemView = itemView;
posterIv = itemView.findViewById(R.id.posterIv);
titleTv = itemView.findViewById(R.id.titleTv);
ratingTv = itemView.findViewById(R.id.ratingTv);
ratingBar = itemView.findViewById(R.id.ratingBar);
// itemView.setOnClickListener(new View.OnClickListener() {
// @Override
// public void onClick(View v) {
// Log.d(TAG, "aaaaaaa");
// }
// });
}
public void setItem(Movie movie) {
titleTv.setText(movie.getTitle());
ratingTv.setText(String.valueOf(movie.getRating()));
Glide.with(posterIv.getContext())
.load(movie.getMediumCoverImage())
.placeholder(R.drawable.round_image)
.transform(new FitCenter(), new RoundedCorners(20))
.into(posterIv);
ratingBar.setRating(movie.getRating());
}
}
// 통신으로 데이터가 전달 되면 여기 메서드로 데이를 전달 받게 한다.
// @SuppressLint("NotifyDataSetChanged")
// public void addItems(List<Movie> list) {
// this.list = list;
// this.notifyDataSetChanged();
// }
//
// public void EachItem(Movie movie) {
// this.list.add(movie);
// this.notifyDataSetChanged();
// }
@SuppressLint("NotifyDataSetChanged")
public void addItem(List<Movie> addList) {
list.addAll(list.size(), addList);
notifyDataSetChanged();
}
}
interfaces>OnPageTypeChange.interface
package com.example.myapplication.interfaces;
public interface OnPageTypeChange {
void typeToolbarChange(String title);
}
models>Data.java
package com.example.myapplication.models;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
import java.util.List;
public class Data {
@SerializedName("movie_count")
@Expose
private Integer movieCount;
@SerializedName("limit")
@Expose
private Integer limit;
@SerializedName("page_number")
@Expose
private Integer pageNumber;
@SerializedName("movies")
@Expose
private List<Movie> movies = null;
public Integer getMovieCount() {
return movieCount;
}
public void setMovieCount(Integer movieCount) {
this.movieCount = movieCount;
}
public Integer getLimit() {
return limit;
}
public void setLimit(Integer limit) {
this.limit = limit;
}
public Integer getPageNumber() {
return pageNumber;
}
public void setPageNumber(Integer pageNumber) {
this.pageNumber = pageNumber;
}
public List<Movie> getMovies() {
return movies;
}
public void setMovies(List<Movie> movies) {
this.movies = movies;
}
}
models>Meta.java
package com.example.myapplication.models;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
public class Meta {
@SerializedName("server_time")
@Expose
private Integer serverTime;
@SerializedName("server_timezone")
@Expose
private String serverTimezone;
@SerializedName("api_version")
@Expose
private Integer apiVersion;
@SerializedName("execution_time")
@Expose
private String executionTime;
public Integer getServerTime() {
return serverTime;
}
public void setServerTime(Integer serverTime) {
this.serverTime = serverTime;
}
public String getServerTimezone() {
return serverTimezone;
}
public void setServerTimezone(String serverTimezone) {
this.serverTimezone = serverTimezone;
}
public Integer getApiVersion() {
return apiVersion;
}
public void setApiVersion(Integer apiVersion) {
this.apiVersion = apiVersion;
}
public String getExecutionTime() {
return executionTime;
}
public void setExecutionTime(String executionTime) {
this.executionTime = executionTime;
}
}
models>Movie.java
package com.example.myapplication.models;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
import java.util.List;
public class Movie implements Serializable {
@SerializedName("id")
@Expose
private Integer id;
@SerializedName("url")
@Expose
private String url;
@SerializedName("imdb_code")
@Expose
private String imdbCode;
@SerializedName("title")
@Expose
private String title;
@SerializedName("title_english")
@Expose
private String titleEnglish;
@SerializedName("title_long")
@Expose
private String titleLong;
@SerializedName("slug")
@Expose
private String slug;
@SerializedName("year")
@Expose
private Integer year;
@SerializedName("rating")
@Expose
private Float rating;
@SerializedName("runtime")
@Expose
private Integer runtime;
@SerializedName("genres")
@Expose
private List<String> genres = null;
@SerializedName("summary")
@Expose
private String summary;
@SerializedName("description_full")
@Expose
private String descriptionFull;
@SerializedName("synopsis")
@Expose
private String synopsis;
@SerializedName("yt_trailer_code")
@Expose
private String ytTrailerCode;
@SerializedName("language")
@Expose
private String language;
@SerializedName("mpa_rating")
@Expose
private String mpaRating;
@SerializedName("background_image")
@Expose
private String backgroundImage;
@SerializedName("background_image_original")
@Expose
private String backgroundImageOriginal;
@SerializedName("small_cover_image")
@Expose
private String smallCoverImage;
@SerializedName("medium_cover_image")
@Expose
private String mediumCoverImage;
@SerializedName("large_cover_image")
@Expose
private String largeCoverImage;
@SerializedName("state")
@Expose
private String state;
@SerializedName("date_uploaded")
@Expose
private String dateUploaded;
@SerializedName("date_uploaded_unix")
@Expose
private Integer dateUploadedUnix;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getImdbCode() {
return imdbCode;
}
public void setImdbCode(String imdbCode) {
this.imdbCode = imdbCode;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getTitleEnglish() {
return titleEnglish;
}
public void setTitleEnglish(String titleEnglish) {
this.titleEnglish = titleEnglish;
}
public String getTitleLong() {
return titleLong;
}
public void setTitleLong(String titleLong) {
this.titleLong = titleLong;
}
public String getSlug() {
return slug;
}
public void setSlug(String slug) {
this.slug = slug;
}
public Integer getYear() {
return year;
}
public void setYear(Integer year) {
this.year = year;
}
public Float getRating() {
return rating;
}
public void setRating(Float rating) {
this.rating = rating;
}
public Integer getRuntime() {
return runtime;
}
public void setRuntime(Integer runtime) {
this.runtime = runtime;
}
public List<String> getGenres() {
return genres;
}
public void setGenres(List<String> genres) {
this.genres = genres;
}
public String getSummary() {
return summary;
}
public void setSummary(String summary) {
this.summary = summary;
}
public String getDescriptionFull() {
return descriptionFull;
}
public void setDescriptionFull(String descriptionFull) {
this.descriptionFull = descriptionFull;
}
public String getSynopsis() {
return synopsis;
}
public void setSynopsis(String synopsis) {
this.synopsis = synopsis;
}
public String getYtTrailerCode() {
return ytTrailerCode;
}
public void setYtTrailerCode(String ytTrailerCode) {
this.ytTrailerCode = ytTrailerCode;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
public String getMpaRating() {
return mpaRating;
}
public void setMpaRating(String mpaRating) {
this.mpaRating = mpaRating;
}
public String getBackgroundImage() {
return backgroundImage;
}
public void setBackgroundImage(String backgroundImage) {
this.backgroundImage = backgroundImage;
}
public String getBackgroundImageOriginal() {
return backgroundImageOriginal;
}
public void setBackgroundImageOriginal(String backgroundImageOriginal) {
this.backgroundImageOriginal = backgroundImageOriginal;
}
public String getSmallCoverImage() {
return smallCoverImage;
}
public void setSmallCoverImage(String smallCoverImage) {
this.smallCoverImage = smallCoverImage;
}
public String getMediumCoverImage() {
return mediumCoverImage;
}
public void setMediumCoverImage(String mediumCoverImage) {
this.mediumCoverImage = mediumCoverImage;
}
public String getLargeCoverImage() {
return largeCoverImage;
}
public void setLargeCoverImage(String largeCoverImage) {
this.largeCoverImage = largeCoverImage;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getDateUploaded() {
return dateUploaded;
}
public void setDateUploaded(String dateUploaded) {
this.dateUploaded = dateUploaded;
}
public Integer getDateUploadedUnix() {
return dateUploadedUnix;
}
public void setDateUploadedUnix(Integer dateUploadedUnix) {
this.dateUploadedUnix = dateUploadedUnix;
}
}
models>YtsData.java
package com.example.myapplication.models;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
public class YtsData {
@SerializedName("status")
@Expose
private String status;
@SerializedName("status_message")
@Expose
private String statusMessage;
@SerializedName("data")
@Expose
private Data data;
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getStatusMessage() {
return statusMessage;
}
public void setStatusMessage(String statusMessage) {
this.statusMessage = statusMessage;
}
public Data getData() {
return data;
}
public void setData(Data data) {
this.data = data;
}
}
repository>MovieService.interface
package com.example.myapplication.repository;
import com.example.myapplication.models.YtsData;
import com.example.myapplication.utils.Define;
import retrofit2.Call;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.http.GET;
import retrofit2.http.Query;
// https://yts.lt/api/v2/list_movies.json?limit=20&page=1&sort_by=rating
public interface MovieService {
// public static final
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(Define.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
// dto 만들어주기
@GET("list_movies.json")
Call<YtsData> repoContributors(@Query("sort_by") String sortBy,
@Query("page") int page,
@Query("limit") int limit);
}
utils>BottomSheetFragment.java
package com.example.myapplication;
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.bumptech.glide.Glide;
import com.example.myapplication.databinding.FragmentBottomSheetBinding;
import com.example.myapplication.models.Movie;
public class BottomSheetFragment extends Fragment {
private FragmentBottomSheetBinding binding;
private Movie movie;
public BottomSheetFragment(Movie movie) {
this.movie = movie;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentBottomSheetBinding.inflate(inflater, container, false);
Glide.with(this)
.load(movie.getMediumCoverImage())
.into(binding.movieImageview);
binding.summaryTextView.setText(movie.getSummary());
binding.descriptionTextView.setText(movie.getDescriptionFull());
binding.synopsisTextView.setText(movie.getSynopsis());
return binding.getRoot();
}
}
utils>DetailActivity.java
package com.example.myapplication;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import android.os.Bundle;
import android.util.Log;
import com.bumptech.glide.Glide;
import com.example.myapplication.databinding.ActivityDetailBinding;
import com.example.myapplication.models.Movie;
import com.example.myapplication.utils.Define;
public class DetailActivity extends AppCompatActivity {
private final static String TAG = DetailActivity.class.getName();
private ActivityDetailBinding binding;
private BottomSheetFragment bottomSheetFragment;
private Movie movie;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityDetailBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
if (getIntent() != null) {
movie = (Movie) getIntent().getSerializableExtra(Define.PARAM_MOVIE_OBJ);
initData();
addEventListener();
}
}
private void initData() {
binding.titleTextView.setText(movie.getTitle());
binding.yearTextView.setText( "제작년도: " + movie.getYear() + "년도");
binding.runTimeTextView.setText("상영시간 : " + movie.getRuntime() + "분");
Glide.with(this)
.load(movie.getMediumCoverImage())
.into(binding.moviePoster);
Glide.with(this)
.load(movie.getBackgroundImage())
.into(binding.backgroundImageView);
bottomSheetFragment = new BottomSheetFragment(movie);
}
private void addEventListener() {
binding.showContentButton.setOnClickListener(v -> {
addFragment();
});
}
private void addFragment() {
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
// 잘 알려진 버그 out 시 애니메이션 처리 안됨
transaction.setCustomAnimations(R.anim.slide_in_bottom, R.anim.slide_out_bottom);
transaction.setReorderingAllowed(true);
transaction.addToBackStack("aaa");
transaction.replace(binding.bottomSheetContainer.getId(), bottomSheetFragment);
transaction.commit();
}
}
utils>InfoFragment.java
package com.example.myapplication;
import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.os.Bundle;
import androidx.activity.OnBackPressedCallback;
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.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import com.example.myapplication.databinding.FragmentInfoBinding;
import com.example.myapplication.interfaces.OnPageTypeChange;
import com.example.myapplication.utils.Define;
public class InfoFragment extends Fragment {
private static InfoFragment infoFragment;
private FragmentInfoBinding binding;
private final OnPageTypeChange onPageTypeChange;
private OnBackPressedCallback onBackPressedCallback;
// 싱글톤 패턴 적용
private InfoFragment(OnPageTypeChange onPageTypeChange) {
this.onPageTypeChange = onPageTypeChange;
}
public static InfoFragment getInstance(OnPageTypeChange onPageTypeChange) {
if (infoFragment == null) {
infoFragment = new InfoFragment(onPageTypeChange);
}
return infoFragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
onPageTypeChange.typeToolbarChange("Stub!");
fragmentBackPressCustom();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentInfoBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setupWebView();
}
@SuppressLint("SetJavaScriptEnabled")
private void setupWebView() {
WebView webView = binding.webView;
webView.setWebViewClient(new WebViewClient() {
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
binding.progressIndicator.setVisibility(View.GONE);
}
// @Override
// public void onPageFinished(WebView view, String url) {
// super.onPageFinished(view, url);
// // 웹뷰가 렌더링이 다 되었을 때 콜백 되는 메서드
// // HTML 뼈대, CSS , javascript
// }
});
webView.loadUrl(Define.WEB_VIEW_URL);
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
}
// 프래그먼에서 뒤로가기 이벤트 커스텀 하기
private void fragmentBackPressCustom() {
onBackPressedCallback = new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
Log.d("TAG", "stub!");
}
};
requireActivity().
getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), onBackPressedCallback);
}
@Override
public void onDetach() {
super.onDetach();
onBackPressedCallback.remove();
}
}
utils>MainActivity.java
package com.example.myapplication;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import android.os.Bundle;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import com.example.myapplication.databinding.ActivityMainBinding;
import com.example.myapplication.interfaces.OnPageTypeChange;
import com.example.myapplication.models.YtsData;
import com.example.myapplication.repository.MovieService;
import com.example.myapplication.utils.Define;
import com.example.myapplication.utils.FragmentType;
import com.google.android.material.appbar.MaterialToolbar;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class MainActivity extends AppCompatActivity implements OnPageTypeChange {
// 1. viewBind 사용하기
private ActivityMainBinding binding;
private static final String TAG = MainActivity.class.getName();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 뷰 바인딩 설정 방법 (Activity)
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
intData();
}
private void intData() {
addTopAppbarEventListener();
addBottomNavigationListener();
replaceFragment(FragmentType.MOVIE);
}
private void addTopAppbarEventListener() {
binding.topAppBar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
if (item.getItemId() == R.id.favorite) {
// 새로운 화면을 띄움 !!
// 도전 Log.d(TAG, "1111111");
}
return true;
}
});
}
private void addBottomNavigationListener() {
binding.bottomNavigation.setOnItemSelectedListener(item -> {
switch (item.getItemId()) {
case R.id.page_1:
replaceFragment(FragmentType.MOVIE);
break;
case R.id.page_2:
replaceFragment(FragmentType.INFO);
break;
}
return true;
});
}
private void replaceFragment(FragmentType type) {
Fragment fragment;
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
if (type == FragmentType.MOVIE) {
fragment = MovieFragment.getInstance(this);
} else {
fragment = InfoFragment.getInstance(this);
} //movie , info
transaction.replace(binding.mainContainer.getId(), fragment, type.toString());
transaction.commit();
}
/**
* 콜백 메서드
*
* @param title : app title 변수
*/
@Override
public void typeToolbarChange(String title) {
// 여기에 알람이 옴.
MaterialToolbar toolbar = binding.topAppBar;
if (title.equals(Define.PAGE_TITLE_MOVIE)) {
toolbar.setTitle(title);
toolbar.setVisibility(View.VISIBLE);
} else {
toolbar.setVisibility(View.GONE);
}
}
@Override
public void onBackPressed() {
Log.d("TAG", "액티비티에서 이벤트 캐치");
//super.onBackPressed();
// 여기에서 이벤트가 들어 왔을 때
// 현재 화면에 movie, info 확인을 한 다음
// info --> movie(화면에 그려줘)
// movie --> (앱 종료)
//INFO
String fragmentByTag = getSupportFragmentManager()
.findFragmentByTag(FragmentType.INFO.toString()).getTag();
if (fragmentByTag.equals(FragmentType.INFO.toString())) {
replaceFragment(FragmentType.MOVIE);
} else {
super.onBackPressed();
}
}
}
utils>MovieFragment.java
package com.example.myapplication;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import com.example.myapplication.adapter.MovieAdapter;
import com.example.myapplication.databinding.FragmentMovieBinding;
import com.example.myapplication.interfaces.OnPageTypeChange;
import com.example.myapplication.models.Movie;
import com.example.myapplication.models.YtsData;
import com.example.myapplication.repository.MovieService;
import com.example.myapplication.utils.Define;
import java.util.ArrayList;
import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class MovieFragment extends Fragment {
private static final String TAG = MovieFragment.class.getName();
private FragmentMovieBinding binding;// view Binding
private static MovieFragment movieFragment; // 싱글톤 패턴
private MovieAdapter movieAdapter;
private MovieService service;
private final OnPageTypeChange onPageTypeChange;
private int currentPageNumber = 1;
private static final int DATA_LIMIT = 10;
private List<Movie> movieList = new ArrayList<>();
private boolean preventDuplicateScrollEvent = true;
private boolean isFirstFragmentStart = true;
private MovieFragment(OnPageTypeChange onPageTypeChange) {
this.onPageTypeChange = onPageTypeChange;
}
public static MovieFragment getInstance(OnPageTypeChange onPageTypeChange) {
if (movieFragment == null) {
movieFragment = new MovieFragment(onPageTypeChange);
}
return movieFragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
service = MovieService.retrofit.create(MovieService.class);
onPageTypeChange.typeToolbarChange(Define.PAGE_TITLE_MOVIE);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// 플래그먼트에서 view binding 사용 방법
binding = FragmentMovieBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initRecyclerView();
if (isFirstFragmentStart) {
requestMoviesData(currentPageNumber);
} else {
binding.progressIndicator.setVisibility(View.GONE);
}
}
private void initRecyclerView() {
// 1. 어댑터
// 2. 매니저
// 3. 셋팅
movieAdapter = new MovieAdapter(getContext());
movieAdapter.addItem(movieList);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getContext());
final RecyclerView recyclerView = binding.movieRecyclerView;
recyclerView.setAdapter(movieAdapter);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.hasFixedSize();
recyclerView.setOnScrollChangeListener(new View.OnScrollChangeListener() {
@Override
public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
if (preventDuplicateScrollEvent) {
int lastVisibleItemPosition = ((LinearLayoutManager) recyclerView.getLayoutManager()).findLastVisibleItemPosition();
int itemTotalCount = recyclerView.getAdapter().getItemCount() - 1;
if (lastVisibleItemPosition == itemTotalCount) {
// Toast.makeText(getContext(), "마지막 위치 입니다!", Toast.LENGTH_SHORT).show();
if (currentPageNumber != 1) {
requestMoviesData(currentPageNumber);
preventDuplicateScrollEvent = false;
}
}
}
}
});
}
private void requestMoviesData(int page) {
String orderBy = "rating";
service.repoContributors(orderBy, page, DATA_LIMIT)
.enqueue(new Callback<YtsData>() {
@Override
public void onResponse(Call<YtsData> call, Response<YtsData> response) {
if (response.isSuccessful()) {
YtsData ytsData = response.body();
// List<Movie> list = ytsData.getData().getMovies();
movieList = ytsData.getData().getMovies();
movieAdapter.addItem(ytsData.getData().getMovies());
currentPageNumber++; // 2
preventDuplicateScrollEvent = true;
isFirstFragmentStart = false;
binding.progressIndicator.setVisibility(View.GONE);
} else {
//assert 란 개발자들이 디버깅을 빠르게 하기위한 도구
//즉, 에러 검출용 코드이지 코를 다 완성하고 동작할 때 돌아가는 함수가 아니다.
//log 보다 더 효율적으로 사용될 수 있다.
//컴파일, 실제 앱을 배포(컴파일러가 무시)
assert response.errorBody() != null;
Log.d(TAG, response.errorBody().toString());
}
}
@Override
public void onFailure(Call<YtsData> call, Throwable t) {
Log.d(TAG, t.getMessage());
}
});
}
}
utils>SplashActivity.java
package com.example.myapplication;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.TextView;
public class SplashActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
);
TextView textView = findViewById(R.id.splashTextView);
Animation slideAnimation = AnimationUtils.loadAnimation(this, R.anim.slide_side);
textView.startAnimation(slideAnimation);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
startActivity(intent);
finish();
}
}, 2000);
}
}
anim>slide_in_bottom.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="500"
android:fromYDelta="100%"
android:toYDelta="0%" />
</set>
anim>slide_out_bottom.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="500"
android:fromYDelta="0%"
android:toYDelta="100%" />
</set>
anim>slide_side.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="2000"
android:fromXDelta="-100%"
android:fromYDelta="0%" />
<alpha
android:duration="2000"
android:fromAlpha="0.1"
android:toAlpha="1.0" />
</set>
drawable>ic_baseline_favorite_24.xml => ❤ icon
drawable>ic_baseline_info_24.xml => ⓘ icon
drawable>ic_baseline_movie_24.xml => 🎬 icon
drawable>round_image.png
drawable>top_round.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:topLeftRadius="30dp"
android:topRightRadius="30dp" />
<solid android:color="@color/white" />
<stroke
android:width="1dp"
android:color="@color/brand_100" />
</shape>
layout>activity_detail.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".DetailActivity">
<ImageView
android:id="@+id/backgroundImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black" />
<TextView
android:id="@+id/titleTextView"
style="@style/info_text_style"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxWidth="250dp"
android:text="dlaksjdhflkjashdflkjahsdflkjhasdlfkjalksjdhflkajshd"
android:textAlignment="center"
app:layout_constraintBottom_toTopOf="@id/yearTextView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/yearTextView"
style="@style/info_text_style"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxWidth="250dp"
android:text="2020"
android:textAlignment="center"
app:layout_constraintBottom_toTopOf="@id/runTimeTextView"
app:layout_constraintEnd_toEndOf="@id/titleTextView"
app:layout_constraintStart_toStartOf="@id/titleTextView"
app:layout_constraintTop_toBottomOf="@id/titleTextView" />
<TextView
android:id="@+id/runTimeTextView"
style="@style/info_text_style"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxWidth="250dp"
android:text="120분"
android:textAlignment="center"
app:layout_constraintBottom_toTopOf="@id/moviePoster"
app:layout_constraintEnd_toEndOf="@id/titleTextView"
app:layout_constraintStart_toStartOf="@id/titleTextView"
app:layout_constraintTop_toBottomOf="@id/yearTextView" />
<ImageView
android:id="@+id/moviePoster"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:src="@drawable/round_image"
app:layout_constraintBottom_toTopOf="@id/showContentButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/runTimeTextView" />
<Button
android:id="@+id/showContentButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/str_show_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/moviePoster"
app:layout_constraintStart_toStartOf="@id/moviePoster"
app:layout_constraintTop_toBottomOf="@id/moviePoster" />
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:id="@+id/bottomSheetContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:isScrollContainer="true"
android:orientation="vertical"
tools:layout_height="500dp" />
</FrameLayout>
layout>activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/topAppBar"
style="@style/Widget.MaterialComponents.Toolbar.Primary"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:menu="@menu/top_app_bar"
app:title="@string/app_name" />
<LinearLayout
android:id="@+id/mainContainer"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@id/bottomNaviContainer"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/topAppBar" />
<LinearLayout
android:id="@+id/bottomNaviContainer"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/mainContainer"
tools:layout_height="48dp">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomNavigation"
style="@style/Widget.MaterialComponents.BottomNavigationView.Colored"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:menu="@menu/bottom_navigation_menu" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
layout>activity_splash.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
tools:context=".SplashActivity">
<TextView
android:id="@+id/splashTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textColor="@color/brand_100"
android:textSize="40sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
layout>fragment_bottom_sheet.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".BottomSheetFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="500dp"
android:layout_marginTop="100dp"
android:background="@drawable/top_round">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="50dp"
android:orientation="vertical"
android:paddingStart="10dp"
android:paddingEnd="10dp">
<TextView
android:id="@+id/summaryTextView"
style="@style/bottom_sheet_text_style"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="" />
<TextView
android:id="@+id/descriptionTextView"
style="@style/bottom_sheet_text_style"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="" />
<TextView
android:id="@+id/synopsisTextView"
style="@style/bottom_sheet_text_style"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="" />
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/movieImageview"
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_gravity="center_horizontal"
android:src="@drawable/round_image"
app:civ_border_color="@color/success"
app:civ_border_width="2dp" />
</FrameLayout>
layout>fragment_info.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".InfoFragment">
<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressIndicator"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true" />
</FrameLayout>
layout>fragment_movie.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MovieFragment">
<!-- RecyclerView 선언 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/movieRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:listitem="@layout/item_movie_card" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressIndicator"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
layout>item_movie_card.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_margin="20dp"
android:backgroundTint="@color/danger"
app:cardCornerRadius="10dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline1"
android:layout_width="1dp"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintGuide_begin="135dp" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline2"
android:layout_width="1dp"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintGuide_end="16dp" />
<TextView
android:id="@+id/titleTv"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="end"
android:maxLines="2"
android:textColor="#000000"
app:layout_constraintBottom_toTopOf="@id/ratingTv"
app:layout_constraintEnd_toEndOf="@id/guideline2"
app:layout_constraintStart_toStartOf="@id/guideline1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.7"
app:layout_constraintVertical_chainStyle="packed"
tools:text="벤자민의 시간은 꺼꾸로 간다 " />
<TextView
android:id="@+id/ratingTv"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="end"
android:text="9.9"
app:layout_constraintBottom_toTopOf="@id/ratingBar"
app:layout_constraintEnd_toEndOf="@id/guideline2"
app:layout_constraintStart_toStartOf="@id/guideline1"
app:layout_constraintTop_toBottomOf="@id/titleTv" />
<RatingBar
android:id="@+id/ratingBar"
style="?android:ratingBarStyleSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:numStars="10"
android:stepSize="1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/guideline2"
app:layout_constraintStart_toStartOf="@id/guideline1"
app:layout_constraintTop_toBottomOf="@id/ratingTv"
tools:rating="1" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</RelativeLayout>
<ImageView
android:id="@+id/posterIv"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginStart="35dp"
android:layout_marginTop="10dp"
android:scaleType="fitCenter"
android:src="@drawable/round_image" />
</FrameLayout>
menu>bottom_navigation_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/page_1"
android:icon="@drawable/ic_baseline_movie_24"
android:title="@string/str_movie" />
<item
android:id="@+id/page_2"
android:icon="@drawable/ic_baseline_info_24"
android:title="@string/str_info" />
</menu>
menu>top>app_bar.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/favorite"
android:icon="@drawable/ic_baseline_favorite_24"
android:title="@string/str_favorite"
app:showAsAction="ifRoom" />
</menu>
color.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="brand_100">#102A70</color>
<color name="success">#547702</color>
<color name="info">#008AA5</color>
<color name="warning">#BC7203</color>
<color name="danger">#8E1818</color>
</resources>
string.xml
<resources>
<string name="app_name">YTS MOVIE</string>
<string name="str_favorite">favorite</string>
<string name="str_movie">movie</string>
<string name="str_info">info</string>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
<string name="str_show_content">show content</string>
<string name="str_temp">제작연도 %s</string>
</resources>
style.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="info_text_style" parent="android:Widget.TextView">
<item name="android:textColor">@color/info</item>
<item name="android:textSize">20sp</item>
<item name="android:textStyle">bold</item>
<item name="android:layout_marginBottom">12dp</item>
</style>
<style name="bottom_sheet_text_style" parent="android:Widget.TextView">
<item name="android:textColor">@color/black</item>
<item name="android:textSize">16sp</item>
<item name="android:textStyle">bold</item>
<item name="android:layout_marginBottom">16dp</item>
<item name="android:gravity">start|top</item>
</style>
</resources>
themes>themes.xml
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.MyApplication" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/brand_100</item>
<item name="colorPrimaryVariant">@color/info</item>
<item name="colorOnPrimary">@color/success</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/warning</item>
<item name="colorSecondaryVariant">@color/danger</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">@color/brand_100</item>
<!-- Customize your theme here. -->
</style>
</resources>