1 /* 2 * Copyright (C) 2015 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.documentsui.dirlist; 18 19 import android.content.Context; 20 import android.database.Cursor; 21 import android.support.v7.widget.GridLayoutManager; 22 import android.support.v7.widget.RecyclerView.AdapterDataObserver; 23 import android.util.SparseArray; 24 import android.view.ViewGroup; 25 import android.widget.Space; 26 27 import com.android.documentsui.R; 28 import com.android.documentsui.State; 29 30 import java.util.List; 31 32 /** 33 * Adapter wrapper that inserts a sort of line break item between directories and regular files. 34 * Only needs to be used in GRID mode...at this time. 35 */ 36 final class SectionBreakDocumentsAdapterWrapper extends DocumentsAdapter { 37 38 private static final String TAG = "SectionBreakDocumentsAdapterWrapper"; 39 private static final int ITEM_TYPE_SECTION_BREAK = Integer.MAX_VALUE; 40 41 private final Environment mEnv; 42 private final DocumentsAdapter mDelegate; 43 44 private int mBreakPosition = -1; 45 46 SectionBreakDocumentsAdapterWrapper(Environment environment, DocumentsAdapter delegate) { 47 mEnv = environment; 48 mDelegate = delegate; 49 50 // Relay events published by our delegate to our listeners (presumably RecyclerView) 51 // with adjusted positions. 52 mDelegate.registerAdapterDataObserver(new EventRelay()); 53 } 54 55 public GridLayoutManager.SpanSizeLookup createSpanSizeLookup() { 56 return new GridLayoutManager.SpanSizeLookup() { 57 @Override 58 public int getSpanSize(int position) { 59 // Make layout whitespace span the grid. This has the effect of breaking 60 // grid rows whenever layout whitespace is encountered. 61 if (getItemViewType(position) == ITEM_TYPE_SECTION_BREAK) { 62 return mEnv.getColumnCount(); 63 } else { 64 return 1; 65 } 66 } 67 }; 68 } 69 70 @Override 71 public DocumentHolder onCreateViewHolder(ViewGroup parent, int viewType) { 72 if (viewType == ITEM_TYPE_SECTION_BREAK) { 73 return new EmptyDocumentHolder(mEnv.getContext()); 74 } else { 75 return mDelegate.createViewHolder(parent, viewType); 76 } 77 } 78 79 @Override 80 public void onBindViewHolder(DocumentHolder holder, int p, List<Object> payload) { 81 if (holder.getItemViewType() != ITEM_TYPE_SECTION_BREAK) { 82 mDelegate.onBindViewHolder(holder, toDelegatePosition(p), payload); 83 } else { 84 ((EmptyDocumentHolder)holder).bind(mEnv.getDisplayState()); 85 } 86 } 87 88 @Override 89 public void onBindViewHolder(DocumentHolder holder, int p) { 90 if (holder.getItemViewType() != ITEM_TYPE_SECTION_BREAK) { 91 mDelegate.onBindViewHolder(holder, toDelegatePosition(p)); 92 } else { 93 ((EmptyDocumentHolder)holder).bind(mEnv.getDisplayState()); 94 } 95 } 96 97 @Override 98 public int getItemCount() { 99 return mBreakPosition == -1 100 ? mDelegate.getItemCount() 101 : mDelegate.getItemCount() + 1; 102 } 103 104 @Override 105 public void onModelUpdate(Model model) { 106 mDelegate.onModelUpdate(model); 107 mBreakPosition = -1; 108 109 // Walk down the list of IDs till we encounter something that's not a directory, and 110 // insert a whitespace element - this introduces a visual break in the grid between 111 // folders and documents. 112 // TODO: This code makes assumptions about the model, namely, that it performs a 113 // bucketed sort where directories will always be ordered before other files. CBB. 114 List<String> modelIds = mDelegate.getModelIds(); 115 for (int i = 0; i < modelIds.size(); i++) { 116 if (!isDirectory(model, i)) { 117 // If the break is the first thing in the list, then there are actually no 118 // directories. In that case, don't insert a break at all. 119 if (i > 0) { 120 mBreakPosition = i; 121 } 122 break; 123 } 124 } 125 } 126 127 @Override 128 public void onModelUpdateFailed(Exception e) { 129 mDelegate.onModelUpdateFailed(e); 130 } 131 132 @Override 133 public int getItemViewType(int p) { 134 if (p == mBreakPosition) { 135 return ITEM_TYPE_SECTION_BREAK; 136 } else { 137 return mDelegate.getItemViewType(toDelegatePosition(p)); 138 } 139 } 140 141 /** 142 * Returns the position of an item in the delegate, adjusting 143 * values that are greater than the break position. 144 * 145 * @param p Position within the view 146 * @return Position within the delegate 147 */ 148 private int toDelegatePosition(int p) { 149 return (mBreakPosition != -1 && p > mBreakPosition) ? p - 1 : p; 150 } 151 152 /** 153 * Returns the position of an item in the view, adjusting 154 * values that are greater than the break position. 155 * 156 * @param p Position within the delegate 157 * @return Position within the view 158 */ 159 private int toViewPosition(int p) { 160 // If position is greater than or equal to the break, increase by one. 161 return (mBreakPosition != -1 && p >= mBreakPosition) ? p + 1 : p; 162 } 163 164 @Override 165 public SparseArray<String> hide(String... ids) { 166 // NOTE: We hear about these changes and adjust break position 167 // in our AdapterDataObserver. 168 return mDelegate.hide(ids); 169 } 170 171 @Override 172 List<String> getModelIds() { 173 return mDelegate.getModelIds(); 174 } 175 176 @Override 177 String getModelId(int p) { 178 return (p == mBreakPosition) ? null : mDelegate.getModelId(toDelegatePosition(p)); 179 } 180 181 @Override 182 public void onItemSelectionChanged(String id) { 183 mDelegate.onItemSelectionChanged(id); 184 } 185 186 // Listener we add to our delegate. This allows us to relay events published 187 // by the delegate to our listeners (presumably RecyclerView) with adjusted positions. 188 private final class EventRelay extends AdapterDataObserver { 189 public void onChanged() { 190 throw new UnsupportedOperationException(); 191 } 192 193 public void onItemRangeChanged(int positionStart, int itemCount) { 194 throw new UnsupportedOperationException(); 195 } 196 197 public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { 198 assert(itemCount == 1); 199 notifyItemRangeChanged(toViewPosition(positionStart), itemCount, payload); 200 } 201 202 public void onItemRangeInserted(int positionStart, int itemCount) { 203 assert(itemCount == 1); 204 if (positionStart < mBreakPosition) { 205 mBreakPosition++; 206 } 207 notifyItemRangeInserted(toViewPosition(positionStart), itemCount); 208 } 209 210 public void onItemRangeRemoved(int positionStart, int itemCount) { 211 assert(itemCount == 1); 212 if (positionStart < mBreakPosition) { 213 mBreakPosition--; 214 } 215 notifyItemRangeRemoved(toViewPosition(positionStart), itemCount); 216 } 217 218 public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { 219 throw new UnsupportedOperationException(); 220 } 221 } 222 223 /** 224 * The most elegant transparent blank box that spans N rows ever conceived. 225 */ 226 private static final class EmptyDocumentHolder extends DocumentHolder { 227 final int mVisibleHeight; 228 229 public EmptyDocumentHolder(Context context) { 230 super(context, new Space(context)); 231 232 // Per UX spec, this puts a bigger gap between the folders and documents in the grid. 233 mVisibleHeight = context.getResources().getDimensionPixelSize( 234 R.dimen.grid_item_margin); 235 } 236 237 public void bind(State state) { 238 bind(null, null, state); 239 } 240 241 @Override 242 public void bind(Cursor cursor, String modelId, State state) { 243 if (state.derivedMode == State.MODE_GRID) { 244 itemView.setMinimumHeight(mVisibleHeight); 245 } else { 246 itemView.setMinimumHeight(0); 247 } 248 return; 249 } 250 } 251 } 252