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.Bitmap; 21 import android.net.Uri; 22 import android.os.Bundle; 23 import android.util.SparseIntArray; 24 import android.view.LayoutInflater; 25 import android.view.View; 26 import android.view.ViewGroup; 27 28 import androidx.annotation.NonNull; 29 import androidx.annotation.Nullable; 30 import androidx.annotation.UiThread; 31 import androidx.fragment.app.Fragment; 32 import androidx.recyclerview.widget.GridLayoutManager; 33 import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup; 34 import androidx.recyclerview.widget.RecyclerView; 35 import androidx.recyclerview.widget.RecyclerView.Adapter; 36 import androidx.recyclerview.widget.RecyclerView.ViewHolder; 37 38 import com.android.pump.R; 39 import com.android.pump.activity.OtherDetailsActivity; 40 import com.android.pump.db.MediaDb; 41 import com.android.pump.db.Other; 42 import com.android.pump.util.Globals; 43 import com.android.pump.util.ImageLoader; 44 import com.android.pump.util.Orientation; 45 import com.android.pump.widget.UriImageView; 46 47 import java.util.List; 48 49 @UiThread 50 public class OtherFragment extends Fragment { 51 private static final int SPAN_COUNT = 6; 52 53 private RecyclerView mRecyclerView; 54 newInstance()55 public static @NonNull Fragment newInstance() { 56 return new OtherFragment(); 57 } 58 59 @Override onCreateView(@onNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)60 public @NonNull View onCreateView(@NonNull LayoutInflater inflater, 61 @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 62 View view = inflater.inflate(R.layout.fragment_other, container, false); 63 mRecyclerView = view.findViewById(R.id.fragment_other_recycler_view); 64 mRecyclerView.setHasFixedSize(true); 65 66 OtherAdapter otherAdapter = new OtherAdapter(requireContext()); 67 mRecyclerView.setAdapter(otherAdapter); 68 69 GridLayoutManager gridLayoutManager = (GridLayoutManager) mRecyclerView.getLayoutManager(); 70 gridLayoutManager.setSpanSizeLookup(otherAdapter.getSpanSizeLookup()); 71 if (gridLayoutManager.getSpanCount() != SPAN_COUNT) { 72 throw new IllegalArgumentException("Expected a span count of " + SPAN_COUNT + 73 ", found a span count of " + gridLayoutManager.getSpanCount() + "."); 74 } 75 76 mRecyclerView.setItemAnimator(null); // TODO Re-enable add/remove animations 77 78 // TODO(b/123707260) Enable view caching 79 //mRecyclerView.setItemViewCacheSize(0); 80 //mRecyclerView.setRecycledViewPool(Globals.getRecycledViewPool(requireContext())); 81 return view; 82 } 83 84 private static class OtherAdapter extends Adapter<ViewHolder> 85 implements MediaDb.UpdateCallback, ImageLoader.Callback { 86 private final ImageLoader mImageLoader; 87 private final MediaDb mMediaDb; 88 private final List<Other> mOthers; // TODO(b/123710968) Use android.support.v7.util.SortedList/android.support.v7.widget.util.SortedListAdapterCallback instead 89 private final SparseIntArray mSpanSize = new SparseIntArray(); 90 OtherAdapter(@onNull Context context)91 private OtherAdapter(@NonNull Context context) { 92 setHasStableIds(true); 93 94 mImageLoader = Globals.getImageLoader(context); 95 mMediaDb = Globals.getMediaDb(context); 96 mOthers = mMediaDb.getOthers(); 97 recalculateSpans(); 98 } 99 onAttachedToRecyclerView(@onNull RecyclerView recyclerView)100 public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { 101 mMediaDb.addOtherUpdateCallback(this); 102 mImageLoader.addCallback(this); 103 } 104 onDetachedFromRecyclerView(@onNull RecyclerView recyclerView)105 public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { 106 mMediaDb.removeOtherUpdateCallback(this); 107 mImageLoader.removeCallback(this); 108 } 109 110 @Override onCreateViewHolder(@onNull ViewGroup parent, int viewType)111 public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 112 if (viewType == R.layout.header) { 113 return new ViewHolder(LayoutInflater.from(parent.getContext()) 114 .inflate(viewType, parent, false)) { }; 115 } else { 116 return new OtherViewHolder(LayoutInflater.from(parent.getContext()) 117 .inflate(viewType, parent, false)); 118 } 119 } 120 121 @Override onBindViewHolder(@onNull ViewHolder holder, int position)122 public void onBindViewHolder(@NonNull ViewHolder holder, int position) { 123 if (position == 0) { 124 // TODO Handle header view 125 } else { 126 Other other = mOthers.get(position - 1); 127 mMediaDb.loadData(other); // TODO Where should we call this? In bind()? 128 ((OtherViewHolder) holder).bind(other); 129 } 130 } 131 132 @Override getItemCount()133 public int getItemCount() { 134 return mOthers.size() + 1; 135 } 136 137 @Override getItemId(int position)138 public long getItemId(int position) { 139 return position == 0 ? -1 : mOthers.get(position - 1).getId(); 140 } 141 142 @Override getItemViewType(int position)143 public int getItemViewType(int position) { 144 return position == 0 ? R.layout.header : R.layout.other; 145 } 146 147 @Override onImageLoaded(@onNull Uri uri, @Nullable Bitmap bitmap)148 public void onImageLoaded(@NonNull Uri uri, @Nullable Bitmap bitmap) { 149 // TODO Optimize this (only update necessary parts -- not the whole world) 150 recalculateSpans(); 151 notifyItemRangeChanged(1, mOthers.size()); 152 } 153 154 @Override onItemsInserted(int index, int count)155 public void onItemsInserted(int index, int count) { 156 notifyItemRangeInserted(index + 1, count); 157 } 158 159 @Override onItemsUpdated(int index, int count)160 public void onItemsUpdated(int index, int count) { 161 notifyItemRangeChanged(index + 1, count); 162 } 163 164 @Override onItemsRemoved(int index, int count)165 public void onItemsRemoved(int index, int count) { 166 notifyItemRangeRemoved(index + 1, count); 167 } 168 recalculateSpans()169 private void recalculateSpans() { 170 // TODO Recalculate when an image is loaded 171 // TODO Recalculate when notifyXxx is called 172 // TODO Optimize 173 mSpanSize.clear(); 174 int current = 0; 175 while (current < mOthers.size()) { 176 int orientation = getOrientation(current); 177 if (orientation == Orientation.LANDSCAPE) { 178 orientation = getOrientation(current + 1); 179 if (orientation == Orientation.LANDSCAPE) { 180 // L L 181 mSpanSize.append(current++, SPAN_COUNT / 2); 182 mSpanSize.append(current++, SPAN_COUNT / 2); 183 } else if (orientation == Orientation.PORTRAIT) { 184 // L P 185 mSpanSize.append(current++, SPAN_COUNT * 2 / 3); 186 mSpanSize.append(current++, SPAN_COUNT * 1 / 3); 187 } else { 188 // L 189 mSpanSize.append(current++, SPAN_COUNT); 190 } 191 } else if (orientation == Orientation.PORTRAIT) { 192 orientation = getOrientation(current + 1); 193 if (orientation == Orientation.LANDSCAPE) { 194 // P L 195 mSpanSize.append(current++, SPAN_COUNT * 1 / 3); 196 mSpanSize.append(current++, SPAN_COUNT * 2 / 3); 197 } else if (orientation == Orientation.PORTRAIT && 198 getOrientation(current + 2) == Orientation.PORTRAIT) { 199 // P P P 200 mSpanSize.append(current++, SPAN_COUNT / 3); 201 mSpanSize.append(current++, SPAN_COUNT / 3); 202 mSpanSize.append(current++, SPAN_COUNT / 3); 203 } else { 204 // P 205 mSpanSize.append(current++, SPAN_COUNT); 206 } 207 } else { 208 // unknown 209 mSpanSize.append(current++, SPAN_COUNT); 210 } 211 } 212 } 213 getOrientation(int index)214 private @Orientation int getOrientation(int index) { 215 Uri thumbUri = index >= mOthers.size() ? null : mOthers.get(index).getThumbnailUri(); 216 if (thumbUri == null) { 217 return Orientation.UNKNOWN; 218 } 219 return mImageLoader.getOrientation(thumbUri); 220 } 221 getSpanSizeLookup()222 private @NonNull SpanSizeLookup getSpanSizeLookup() { 223 return new SpanSizeLookup() { 224 @Override 225 public int getSpanSize(int position) { 226 return position == 0 ? SPAN_COUNT : mSpanSize.get(position - 1); 227 } 228 229 @Override 230 public int getSpanIndex(int position, int spanCount) { 231 // TODO Optimize 232 return super.getSpanIndex(position, spanCount); 233 } 234 235 @Override 236 public int getSpanGroupIndex(int adapterPosition, int spanCount) { 237 // TODO Optimize 238 return super.getSpanGroupIndex(adapterPosition, spanCount); 239 } 240 }; 241 } 242 } 243 244 private static class OtherViewHolder extends ViewHolder { 245 private OtherViewHolder(@NonNull View itemView) { 246 super(itemView); 247 } 248 249 private void bind(@NonNull Other other) { 250 UriImageView imageView = itemView.findViewById(R.id.other_image); 251 252 imageView.setImageURI(other.getThumbnailUri()); 253 254 itemView.setOnClickListener((view) -> 255 OtherDetailsActivity.start(view.getContext(), other)); 256 } 257 } 258 } 259