blob: 2a3b82d0a04bade0d822b1eaa06813de5bec489d [file] [log] [blame]
Ben Linb8c54e72016-06-10 12:13:27 -07001/*
2 * Copyright (C) 2016 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
17package com.android.documentsui;
18
19import android.content.Context;
20import android.support.v7.widget.LinearLayoutManager;
21import android.support.v7.widget.RecyclerView;
22import android.util.AttributeSet;
23import android.view.GestureDetector;
Ben Lin340ab172017-01-27 11:41:26 -080024import android.view.KeyEvent;
Ben Linb8c54e72016-06-10 12:13:27 -070025import android.view.LayoutInflater;
26import android.view.MotionEvent;
27import android.view.View;
28import android.view.ViewGroup;
29import android.widget.ImageView;
Ben Linb8c54e72016-06-10 12:13:27 -070030
31import com.android.documentsui.NavigationViewManager.Breadcrumb;
32import com.android.documentsui.NavigationViewManager.Environment;
Steve McKayd0805062016-09-15 14:30:38 -070033import com.android.documentsui.base.DocumentInfo;
34import com.android.documentsui.base.RootInfo;
Ben Lina355d192017-03-03 14:11:57 -080035import com.android.documentsui.dirlist.AccessibilityEventRouter;
Ben Linb8c54e72016-06-10 12:13:27 -070036
37import java.util.function.Consumer;
Ben Line4ffd062017-01-19 15:10:49 -080038import java.util.function.IntConsumer;
Ben Linb8c54e72016-06-10 12:13:27 -070039
40/**
41 * Horizontal implementation of breadcrumb used for tablet / desktop device layouts
42 */
Ben Linba1cb3d2016-06-20 17:08:22 -070043public final class HorizontalBreadcrumb extends RecyclerView
44 implements Breadcrumb, ItemDragListener.DragHost {
45
46 private static final int USER_NO_SCROLL_OFFSET_THRESHOLD = 5;
Ben Linb8c54e72016-06-10 12:13:27 -070047
48 private LinearLayoutManager mLayoutManager;
49 private BreadcrumbAdapter mAdapter;
Ben Line4ffd062017-01-19 15:10:49 -080050 private IntConsumer mClickListener;
Ben Linb8c54e72016-06-10 12:13:27 -070051
52 public HorizontalBreadcrumb(Context context, AttributeSet attrs, int defStyleAttr) {
53 super(context, attrs, defStyleAttr);
54 }
55
56 public HorizontalBreadcrumb(Context context, AttributeSet attrs) {
57 super(context, attrs);
58 }
59
60 public HorizontalBreadcrumb(Context context) {
61 super(context);
62 }
63
64 @Override
65 public void setup(Environment env,
Steve McKayd9caa6a2016-09-15 16:36:45 -070066 com.android.documentsui.base.State state,
Ben Line4ffd062017-01-19 15:10:49 -080067 IntConsumer listener) {
Ben Linb8c54e72016-06-10 12:13:27 -070068
Ben Line4ffd062017-01-19 15:10:49 -080069 mClickListener = listener;
Ben Linb8c54e72016-06-10 12:13:27 -070070 mLayoutManager = new LinearLayoutManager(
71 getContext(), LinearLayoutManager.HORIZONTAL, false);
Ben Linba1cb3d2016-06-20 17:08:22 -070072 mAdapter = new BreadcrumbAdapter(
Ben Lin340ab172017-01-27 11:41:26 -080073 state, env, new ItemDragListener<>(this), this::onKey);
Ben Line4ffd062017-01-19 15:10:49 -080074 // Since we are using GestureDetector to detect click events, a11y services don't know which views
75 // are clickable because we aren't using View.OnClickListener. Thus, we need to use a custom
76 // accessibility delegate to route click events correctly. See AccessibilityClickEventRouter
77 // for more details on how we are routing these a11y events.
78 setAccessibilityDelegateCompat(
Ben Lina355d192017-03-03 14:11:57 -080079 new AccessibilityEventRouter(this,
Ben Line4ffd062017-01-19 15:10:49 -080080 (View child) -> onAccessibilityClick(child)));
Ben Linb8c54e72016-06-10 12:13:27 -070081
82 setLayoutManager(mLayoutManager);
83 addOnItemTouchListener(new ClickListener(getContext(), this::onSingleTapUp));
84 }
85
86 @Override
87 public void show(boolean visibility) {
88 if (visibility) {
89 setVisibility(VISIBLE);
Ben Linba1cb3d2016-06-20 17:08:22 -070090 boolean shouldScroll = !hasUserDefineScrollOffset();
91 if (getAdapter() == null) {
92 setAdapter(mAdapter);
93 } else {
94 int currentItemCount = mAdapter.getItemCount();
95 int lastItemCount = mAdapter.getLastItemSize();
96 if (currentItemCount > lastItemCount) {
97 mAdapter.notifyItemRangeInserted(lastItemCount,
98 currentItemCount - lastItemCount);
99 mAdapter.notifyItemChanged(lastItemCount - 1);
100 } else if (currentItemCount < lastItemCount) {
101 mAdapter.notifyItemRangeRemoved(currentItemCount,
102 lastItemCount - currentItemCount);
103 mAdapter.notifyItemChanged(currentItemCount - 1);
104 }
105 }
106 if (shouldScroll) {
107 mLayoutManager.scrollToPosition(mAdapter.getItemCount() - 1);
108 }
Ben Linb8c54e72016-06-10 12:13:27 -0700109 } else {
110 setVisibility(GONE);
111 setAdapter(null);
112 }
Ben Linba1cb3d2016-06-20 17:08:22 -0700113 mAdapter.updateLastItemSize();
114 }
115
116 private boolean hasUserDefineScrollOffset() {
117 final int maxOffset = computeHorizontalScrollRange() - computeHorizontalScrollExtent();
118 return (maxOffset - computeHorizontalScrollOffset() > USER_NO_SCROLL_OFFSET_THRESHOLD);
Ben Linb8c54e72016-06-10 12:13:27 -0700119 }
120
Ben Line4ffd062017-01-19 15:10:49 -0800121 private boolean onAccessibilityClick(View child) {
122 int pos = getChildAdapterPosition(child);
123 if (pos != getAdapter().getItemCount() - 1) {
124 mClickListener.accept(pos);
125 return true;
126 }
127 return false;
128 }
129
Ben Lin340ab172017-01-27 11:41:26 -0800130 private boolean onKey(View v, int keyCode, KeyEvent event) {
131 switch (keyCode) {
132 case KeyEvent.KEYCODE_ENTER:
133 return onAccessibilityClick(v);
134 default:
135 return false;
136 }
137 }
138
Ben Linb8c54e72016-06-10 12:13:27 -0700139 @Override
140 public void postUpdate() {
141 }
142
Ben Linba1cb3d2016-06-20 17:08:22 -0700143 @Override
144 public void runOnUiThread(Runnable runnable) {
145 post(runnable);
146 }
147
148 @Override
Garfield Tanda2c0f02017-04-11 13:47:58 -0700149 public void setDropTargetHighlight(View v, boolean highlight) {
Ben Linba1cb3d2016-06-20 17:08:22 -0700150 RecyclerView.ViewHolder vh = getChildViewHolder(v);
151 if (vh instanceof BreadcrumbHolder) {
152 ((BreadcrumbHolder) vh).setHighlighted(highlight);
153 }
154 }
155
156 @Override
Garfield Tanda2c0f02017-04-11 13:47:58 -0700157 public void onDragEntered(View v) {
Ben Lin7f72a3c2016-09-27 16:37:28 -0700158 // do nothing
159 }
160
161 @Override
Garfield Tanda2c0f02017-04-11 13:47:58 -0700162 public void onDragExited(View v) {
Ben Lind0202122016-11-10 18:00:12 -0800163 // do nothing
164 }
165
166 @Override
Ben Linba1cb3d2016-06-20 17:08:22 -0700167 public void onViewHovered(View v) {
168 int pos = getChildAdapterPosition(v);
169 if (pos != mAdapter.getItemCount() - 1) {
Ben Line4ffd062017-01-19 15:10:49 -0800170 mClickListener.accept(pos);
Ben Linba1cb3d2016-06-20 17:08:22 -0700171 }
172 }
173
Garfield Tanda2c0f02017-04-11 13:47:58 -0700174 @Override
175 public void onDragEnded() {
176 // do nothing
177 }
178
Ben Linb8c54e72016-06-10 12:13:27 -0700179 private void onSingleTapUp(MotionEvent e) {
180 View itemView = findChildViewUnder(e.getX(), e.getY());
181 int pos = getChildAdapterPosition(itemView);
182 if (pos != mAdapter.getItemCount() - 1) {
Ben Line4ffd062017-01-19 15:10:49 -0800183 mClickListener.accept(pos);
Ben Linb8c54e72016-06-10 12:13:27 -0700184 }
185 }
186
187 private static final class BreadcrumbAdapter
188 extends RecyclerView.Adapter<BreadcrumbHolder> {
189
Ben Linba1cb3d2016-06-20 17:08:22 -0700190 private final Environment mEnv;
Steve McKayd9caa6a2016-09-15 16:36:45 -0700191 private final com.android.documentsui.base.State mState;
Ben Linba1cb3d2016-06-20 17:08:22 -0700192 private final OnDragListener mDragListener;
Ben Lin340ab172017-01-27 11:41:26 -0800193 private final View.OnKeyListener mClickListener;
Ben Linba1cb3d2016-06-20 17:08:22 -0700194 // We keep the old item size so the breadcrumb will only re-render views that are necessary
195 private int mLastItemSize;
Ben Linb8c54e72016-06-10 12:13:27 -0700196
Steve McKayd9caa6a2016-09-15 16:36:45 -0700197 public BreadcrumbAdapter(com.android.documentsui.base.State state,
Ben Linba1cb3d2016-06-20 17:08:22 -0700198 Environment env,
Ben Lin340ab172017-01-27 11:41:26 -0800199 OnDragListener dragListener,
200 View.OnKeyListener clickListener) {
Ben Linb8c54e72016-06-10 12:13:27 -0700201 mState = state;
202 mEnv = env;
Ben Linba1cb3d2016-06-20 17:08:22 -0700203 mDragListener = dragListener;
Ben Lin340ab172017-01-27 11:41:26 -0800204 mClickListener = clickListener;
Ben Linba1cb3d2016-06-20 17:08:22 -0700205 mLastItemSize = mState.stack.size();
Ben Linb8c54e72016-06-10 12:13:27 -0700206 }
207
208 @Override
209 public BreadcrumbHolder onCreateViewHolder(ViewGroup parent, int viewType) {
210 View v = LayoutInflater.from(parent.getContext())
211 .inflate(R.layout.navigation_breadcrumb_item, null);
212 return new BreadcrumbHolder(v);
213 }
214
215 @Override
216 public void onBindViewHolder(BreadcrumbHolder holder, int position) {
217 final DocumentInfo doc = getItem(position);
Ben Lin9cad6352016-07-06 11:58:05 -0700218 final int horizontalPadding = (int) holder.itemView.getResources()
219 .getDimension(R.dimen.breadcrumb_item_padding);
Ben Linb8c54e72016-06-10 12:13:27 -0700220
221 if (position == 0) {
222 final RootInfo root = mEnv.getCurrentRoot();
223 holder.title.setText(root.title);
Ben Lin9cad6352016-07-06 11:58:05 -0700224 holder.title.setPadding(0, 0, horizontalPadding, 0);
Ben Linb8c54e72016-06-10 12:13:27 -0700225 } else {
226 holder.title.setText(doc.displayName);
Ben Lin9cad6352016-07-06 11:58:05 -0700227 holder.title.setPadding(horizontalPadding, 0, horizontalPadding, 0);
Ben Linb8c54e72016-06-10 12:13:27 -0700228 }
229
230 if (position == getItemCount() - 1) {
231 holder.arrow.setVisibility(View.GONE);
Ben Linba1cb3d2016-06-20 17:08:22 -0700232 } else {
233 holder.arrow.setVisibility(View.VISIBLE);
Ben Linb8c54e72016-06-10 12:13:27 -0700234 }
Ben Linba1cb3d2016-06-20 17:08:22 -0700235 holder.itemView.setOnDragListener(mDragListener);
Ben Lin340ab172017-01-27 11:41:26 -0800236 holder.itemView.setOnKeyListener(mClickListener);
Ben Linb8c54e72016-06-10 12:13:27 -0700237 }
238
239 private DocumentInfo getItem(int position) {
Garfield Tan2a837422016-10-19 11:50:45 -0700240 return mState.stack.get(position);
Ben Linb8c54e72016-06-10 12:13:27 -0700241 }
242
243 @Override
244 public int getItemCount() {
245 return mState.stack.size();
246 }
Ben Linba1cb3d2016-06-20 17:08:22 -0700247
248 public int getLastItemSize() {
249 return mLastItemSize;
250 }
251
252 public void updateLastItemSize() {
253 mLastItemSize = mState.stack.size();
254 }
Ben Linb8c54e72016-06-10 12:13:27 -0700255 }
256
257 private static class BreadcrumbHolder extends RecyclerView.ViewHolder {
258
Ben Linba1cb3d2016-06-20 17:08:22 -0700259 protected DragOverTextView title;
Ben Linb8c54e72016-06-10 12:13:27 -0700260 protected ImageView arrow;
261
262 public BreadcrumbHolder(View itemView) {
263 super(itemView);
Ben Linba1cb3d2016-06-20 17:08:22 -0700264 title = (DragOverTextView) itemView.findViewById(R.id.breadcrumb_text);
Ben Linb8c54e72016-06-10 12:13:27 -0700265 arrow = (ImageView) itemView.findViewById(R.id.breadcrumb_arrow);
266 }
Ben Linba1cb3d2016-06-20 17:08:22 -0700267
268 /**
269 * Highlights the associated item view.
270 * @param highlighted
271 */
272 public void setHighlighted(boolean highlighted) {
273 title.setHighlight(highlighted);
274 }
Ben Linb8c54e72016-06-10 12:13:27 -0700275 }
276
277 private static final class ClickListener extends GestureDetector
278 implements OnItemTouchListener {
279
280 public ClickListener(Context context, Consumer<MotionEvent> listener) {
281 super(context, new SimpleOnGestureListener() {
282 @Override
283 public boolean onSingleTapUp(MotionEvent e) {
284 listener.accept(e);
285 return true;
286 }
287 });
288 }
289
290 @Override
291 public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
292 onTouchEvent(e);
293 return false;
294 }
295
296 @Override
297 public void onTouchEvent(RecyclerView rv, MotionEvent e) {
298 onTouchEvent(e);
299 }
300
301 @Override
302 public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
303 }
304 }
305}