1 /*
2  * Copyright 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.pump.fragment;
18 
19 import android.content.Context;
20 import android.graphics.Rect;
21 import android.os.Bundle;
22 import android.util.DisplayMetrics;
23 import android.view.LayoutInflater;
24 import android.view.View;
25 import android.view.ViewGroup;
26 import android.widget.ImageView;
27 import android.widget.TextView;
28 
29 import androidx.annotation.NonNull;
30 import androidx.annotation.Nullable;
31 import androidx.annotation.UiThread;
32 import androidx.core.view.ViewCompat;
33 import androidx.fragment.app.Fragment;
34 import androidx.recyclerview.widget.GridLayoutManager;
35 import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup;
36 import androidx.recyclerview.widget.RecyclerView;
37 import androidx.recyclerview.widget.SimpleItemAnimator;
38 
39 import com.android.pump.R;
40 import com.android.pump.activity.SeriesDetailsActivity;
41 import com.android.pump.db.MediaDb;
42 import com.android.pump.db.Series;
43 import com.android.pump.util.Globals;
44 
45 import java.util.List;
46 
47 @UiThread
48 public class SeriesFragment extends Fragment {
49     private RecyclerView mRecyclerView;
50 
newInstance()51     public static @NonNull Fragment newInstance() {
52         return new SeriesFragment();
53     }
54 
55     @Override
onCreateView(@onNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)56     public @NonNull View onCreateView(@NonNull LayoutInflater inflater,
57             @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
58         View view = inflater.inflate(R.layout.fragment_series, container, false);
59         mRecyclerView = view.findViewById(R.id.fragment_series_recycler_view);
60         mRecyclerView.setHasFixedSize(true);
61         mRecyclerView.setAdapter(new SeriesAdapter(requireContext()));
62         mRecyclerView.addItemDecoration(new SpaceItemDecoration(4, 16));
63 
64         GridLayoutManager gridLayoutManager = (GridLayoutManager) mRecyclerView.getLayoutManager();
65         gridLayoutManager.setSpanSizeLookup(
66                 new HeaderSpanSizeLookup(gridLayoutManager.getSpanCount()));
67 
68         ((SimpleItemAnimator) mRecyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
69 
70         // TODO(b/123707260) Enable view caching
71         //mRecyclerView.setItemViewCacheSize(0);
72         //mRecyclerView.setRecycledViewPool(Globals.getRecycledViewPool(requireContext()));
73         return view;
74     }
75 
76     private static class SeriesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
77             implements MediaDb.UpdateCallback {
78         private final MediaDb mMediaDb;
79         private final List<Series> mSeries; // TODO(b/123710968) Use android.support.v7.util.SortedList/android.support.v7.widget.util.SortedListAdapterCallback instead
80 
SeriesAdapter(@onNull Context context)81         private SeriesAdapter(@NonNull Context context) {
82             // TODO setHasStableIds(true);
83             mMediaDb = Globals.getMediaDb(context);
84             mSeries = mMediaDb.getSeries();
85         }
86 
onAttachedToRecyclerView(@onNull RecyclerView recyclerView)87         public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
88             mMediaDb.addSeriesUpdateCallback(this);
89         }
90 
onDetachedFromRecyclerView(@onNull RecyclerView recyclerView)91         public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
92             mMediaDb.removeSeriesUpdateCallback(this);
93         }
94 
95         @Override
onCreateViewHolder( @onNull ViewGroup parent, int viewType)96         public @NonNull RecyclerView.ViewHolder onCreateViewHolder(
97                 @NonNull ViewGroup parent, int viewType) {
98             if (viewType == R.layout.header) {
99                 return new RecyclerView.ViewHolder(LayoutInflater.from(parent.getContext())
100                         .inflate(viewType, parent, false)) { };
101             } else {
102                 return new SeriesViewHolder(LayoutInflater.from(parent.getContext())
103                         .inflate(viewType, parent, false));
104             }
105         }
106 
107         @Override
onBindViewHolder(@onNull RecyclerView.ViewHolder holder, int position)108         public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
109             if (position == 0) {
110                 // TODO Handle header view
111             } else {
112                 Series series = mSeries.get(position - 1);
113                 mMediaDb.loadData(series); // TODO Where should we call this? In bind()?
114                 ((SeriesViewHolder) holder).bind(series);
115             }
116         }
117 
118         @Override
getItemCount()119         public int getItemCount() {
120             return mSeries.size() + 1;
121         }
122 
123         @Override
getItemId(int position)124         public long getItemId(int position) {
125             return 0; // TODO return position == 0 ? -1 : mSeries.get(position - 1).getId();
126         }
127 
128         @Override
getItemViewType(int position)129         public int getItemViewType(int position) {
130             return position == 0 ? R.layout.header : R.layout.series;
131         }
132 
133         @Override
onItemsInserted(int index, int count)134         public void onItemsInserted(int index, int count) {
135             notifyItemRangeInserted(index + 1, count);
136         }
137 
138         @Override
onItemsUpdated(int index, int count)139         public void onItemsUpdated(int index, int count) {
140             notifyItemRangeChanged(index + 1, count);
141         }
142 
143         @Override
onItemsRemoved(int index, int count)144         public void onItemsRemoved(int index, int count) {
145             notifyItemRangeRemoved(index + 1, count);
146         }
147     }
148 
149     private static class SeriesViewHolder extends RecyclerView.ViewHolder {
SeriesViewHolder(@onNull View itemView)150         private SeriesViewHolder(@NonNull View itemView) {
151             super(itemView);
152         }
153 
bind(@onNull Series series)154         private void bind(@NonNull Series series) {
155             ImageView imageView = itemView.findViewById(R.id.series_image);
156             TextView textView = itemView.findViewById(R.id.series_text);
157 
158             imageView.setImageURI(series.getPosterUri());
159             textView.setText(series.getTitle());
160 
161             itemView.setOnClickListener((view) ->
162                     SeriesDetailsActivity.start(view.getContext(), series));
163         }
164     }
165 
166     private static class SpaceItemDecoration extends RecyclerView.ItemDecoration {
167         private final int mXOffset;
168         private final int mYOffset;
169 
SpaceItemDecoration(int xOffset, int yOffset)170         private SpaceItemDecoration(int xOffset, int yOffset) {
171             mXOffset = xOffset;
172             mYOffset = yOffset;
173         }
174 
175         @Override
getItemOffsets(@onNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state)176         public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
177             DisplayMetrics displayMetrics = new DisplayMetrics();
178             ViewCompat.getDisplay(parent).getMetrics(displayMetrics);
179             outRect.left = outRect.right = (int) Math.ceil(mXOffset * displayMetrics.density);
180             if (parent.getChildAdapterPosition(view) > 0) {
181                 outRect.bottom = (int) Math.ceil(mYOffset * displayMetrics.density);
182             }
183         }
184     }
185 
186     private static class HeaderSpanSizeLookup extends SpanSizeLookup {
187         private final int mSpanCount;
188 
HeaderSpanSizeLookup(int spanCount)189         private HeaderSpanSizeLookup(int spanCount) {
190             mSpanCount = spanCount;
191         }
192 
193         @Override
getSpanSize(int position)194         public int getSpanSize(int position) {
195             return position == 0 ? mSpanCount : 1;
196         }
197 
198         @Override
getSpanIndex(int position, int spanCount)199         public int getSpanIndex(int position, int spanCount) {
200             return position == 0 ? 0 : (position - 1) % spanCount;
201         }
202 
203         @Override
getSpanGroupIndex(int adapterPosition, int spanCount)204         public int getSpanGroupIndex(int adapterPosition, int spanCount) {
205             return adapterPosition == 0 ? 0 : ((adapterPosition - 1) / spanCount) + 1;
206         }
207     }
208 }
209