blob: 6a7cce1ffed24048136165d592d212b68f97dcb6 [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;
Ben Linb8c54e72016-06-10 12:13:27 -070020import android.util.AttributeSet;
21import android.view.GestureDetector;
Ben Lin340ab172017-01-27 11:41:26 -080022import android.view.KeyEvent;
Ben Linb8c54e72016-06-10 12:13:27 -070023import android.view.LayoutInflater;
24import android.view.MotionEvent;
25import android.view.View;
26import android.view.ViewGroup;
27import android.widget.ImageView;
Ben Linb8c54e72016-06-10 12:13:27 -070028
Tony Huang03401d32019-04-17 16:37:16 +080029import androidx.recyclerview.widget.LinearLayoutManager;
30import androidx.recyclerview.widget.RecyclerView;
31
Ben Linb8c54e72016-06-10 12:13:27 -070032import com.android.documentsui.NavigationViewManager.Breadcrumb;
33import com.android.documentsui.NavigationViewManager.Environment;
Steve McKayd0805062016-09-15 14:30:38 -070034import com.android.documentsui.base.DocumentInfo;
35import com.android.documentsui.base.RootInfo;
Ben Lina355d192017-03-03 14:11:57 -080036import com.android.documentsui.dirlist.AccessibilityEventRouter;
Ben Linb8c54e72016-06-10 12:13:27 -070037
38import java.util.function.Consumer;
Ben Line4ffd062017-01-19 15:10:49 -080039import java.util.function.IntConsumer;
Ben Linb8c54e72016-06-10 12:13:27 -070040
41/**
42 * Horizontal implementation of breadcrumb used for tablet / desktop device layouts
43 */
Ben Linba1cb3d2016-06-20 17:08:22 -070044public final class HorizontalBreadcrumb extends RecyclerView
45 implements Breadcrumb, ItemDragListener.DragHost {
46
47 private static final int USER_NO_SCROLL_OFFSET_THRESHOLD = 5;
Ben Linb8c54e72016-06-10 12:13:27 -070048
49 private LinearLayoutManager mLayoutManager;
50 private BreadcrumbAdapter mAdapter;
Ben Line4ffd062017-01-19 15:10:49 -080051 private IntConsumer mClickListener;
Ben Linb8c54e72016-06-10 12:13:27 -070052
53 public HorizontalBreadcrumb(Context context, AttributeSet attrs, int defStyleAttr) {
54 super(context, attrs, defStyleAttr);
55 }
56
57 public HorizontalBreadcrumb(Context context, AttributeSet attrs) {
58 super(context, attrs);
59 }
60
61 public HorizontalBreadcrumb(Context context) {
62 super(context);
63 }
64
65 @Override
66 public void setup(Environment env,
Steve McKayd9caa6a2016-09-15 16:36:45 -070067 com.android.documentsui.base.State state,
Ben Line4ffd062017-01-19 15:10:49 -080068 IntConsumer listener) {
Ben Linb8c54e72016-06-10 12:13:27 -070069
Ben Line4ffd062017-01-19 15:10:49 -080070 mClickListener = listener;
Ben Linb8c54e72016-06-10 12:13:27 -070071 mLayoutManager = new LinearLayoutManager(
72 getContext(), LinearLayoutManager.HORIZONTAL, false);
Ben Linba1cb3d2016-06-20 17:08:22 -070073 mAdapter = new BreadcrumbAdapter(
Ben Lin340ab172017-01-27 11:41:26 -080074 state, env, new ItemDragListener<>(this), this::onKey);
Ben Line4ffd062017-01-19 15:10:49 -080075 // Since we are using GestureDetector to detect click events, a11y services don't know which views
76 // are clickable because we aren't using View.OnClickListener. Thus, we need to use a custom
77 // accessibility delegate to route click events correctly. See AccessibilityClickEventRouter
78 // for more details on how we are routing these a11y events.
79 setAccessibilityDelegateCompat(
Ben Lina355d192017-03-03 14:11:57 -080080 new AccessibilityEventRouter(this,
Tony Huang03401d32019-04-17 16:37:16 +080081 (View child) -> onAccessibilityClick(child), null));
Ben Linb8c54e72016-06-10 12:13:27 -070082
83 setLayoutManager(mLayoutManager);
84 addOnItemTouchListener(new ClickListener(getContext(), this::onSingleTapUp));
85 }
86
87 @Override
88 public void show(boolean visibility) {
89 if (visibility) {
90 setVisibility(VISIBLE);
Ben Linba1cb3d2016-06-20 17:08:22 -070091 boolean shouldScroll = !hasUserDefineScrollOffset();
92 if (getAdapter() == null) {
93 setAdapter(mAdapter);
94 } else {
95 int currentItemCount = mAdapter.getItemCount();
96 int lastItemCount = mAdapter.getLastItemSize();
97 if (currentItemCount > lastItemCount) {
98 mAdapter.notifyItemRangeInserted(lastItemCount,
99 currentItemCount - lastItemCount);
100 mAdapter.notifyItemChanged(lastItemCount - 1);
101 } else if (currentItemCount < lastItemCount) {
102 mAdapter.notifyItemRangeRemoved(currentItemCount,
103 lastItemCount - currentItemCount);
104 mAdapter.notifyItemChanged(currentItemCount - 1);
105 }
106 }
107 if (shouldScroll) {
108 mLayoutManager.scrollToPosition(mAdapter.getItemCount() - 1);
109 }
Ben Linb8c54e72016-06-10 12:13:27 -0700110 } else {
111 setVisibility(GONE);
112 setAdapter(null);
113 }
Ben Linba1cb3d2016-06-20 17:08:22 -0700114 mAdapter.updateLastItemSize();
115 }
116
117 private boolean hasUserDefineScrollOffset() {
118 final int maxOffset = computeHorizontalScrollRange() - computeHorizontalScrollExtent();
119 return (maxOffset - computeHorizontalScrollOffset() > USER_NO_SCROLL_OFFSET_THRESHOLD);
Ben Linb8c54e72016-06-10 12:13:27 -0700120 }
121
Ben Line4ffd062017-01-19 15:10:49 -0800122 private boolean onAccessibilityClick(View child) {
123 int pos = getChildAdapterPosition(child);
124 if (pos != getAdapter().getItemCount() - 1) {
125 mClickListener.accept(pos);
126 return true;
127 }
128 return false;
129 }
130
Ben Lin340ab172017-01-27 11:41:26 -0800131 private boolean onKey(View v, int keyCode, KeyEvent event) {
132 switch (keyCode) {
133 case KeyEvent.KEYCODE_ENTER:
134 return onAccessibilityClick(v);
135 default:
136 return false;
137 }
138 }
139
Ben Linb8c54e72016-06-10 12:13:27 -0700140 @Override
141 public void postUpdate() {
142 }
143
Ben Linba1cb3d2016-06-20 17:08:22 -0700144 @Override
145 public void runOnUiThread(Runnable runnable) {
146 post(runnable);
147 }
148
149 @Override
Garfield Tanda2c0f02017-04-11 13:47:58 -0700150 public void setDropTargetHighlight(View v, boolean highlight) {
Ben Linba1cb3d2016-06-20 17:08:22 -0700151 RecyclerView.ViewHolder vh = getChildViewHolder(v);
152 if (vh instanceof BreadcrumbHolder) {
153 ((BreadcrumbHolder) vh).setHighlighted(highlight);
154 }
155 }
156
157 @Override
Garfield Tanda2c0f02017-04-11 13:47:58 -0700158 public void onDragEntered(View v) {
Ben Lin7f72a3c2016-09-27 16:37:28 -0700159 // do nothing
160 }
161
162 @Override
Garfield Tanda2c0f02017-04-11 13:47:58 -0700163 public void onDragExited(View v) {
Ben Lind0202122016-11-10 18:00:12 -0800164 // do nothing
165 }
166
167 @Override
Ben Linba1cb3d2016-06-20 17:08:22 -0700168 public void onViewHovered(View v) {
169 int pos = getChildAdapterPosition(v);
170 if (pos != mAdapter.getItemCount() - 1) {
Ben Line4ffd062017-01-19 15:10:49 -0800171 mClickListener.accept(pos);
Ben Linba1cb3d2016-06-20 17:08:22 -0700172 }
173 }
174
Garfield Tanda2c0f02017-04-11 13:47:58 -0700175 @Override
176 public void onDragEnded() {
177 // do nothing
178 }
179
Ben Linb8c54e72016-06-10 12:13:27 -0700180 private void onSingleTapUp(MotionEvent e) {
181 View itemView = findChildViewUnder(e.getX(), e.getY());
182 int pos = getChildAdapterPosition(itemView);
183 if (pos != mAdapter.getItemCount() - 1) {
Ben Line4ffd062017-01-19 15:10:49 -0800184 mClickListener.accept(pos);
Ben Linb8c54e72016-06-10 12:13:27 -0700185 }
186 }
187
188 private static final class BreadcrumbAdapter
189 extends RecyclerView.Adapter<BreadcrumbHolder> {
190
Ben Linba1cb3d2016-06-20 17:08:22 -0700191 private final Environment mEnv;
Steve McKayd9caa6a2016-09-15 16:36:45 -0700192 private final com.android.documentsui.base.State mState;
Ben Linba1cb3d2016-06-20 17:08:22 -0700193 private final OnDragListener mDragListener;
Ben Lin340ab172017-01-27 11:41:26 -0800194 private final View.OnKeyListener mClickListener;
Ben Linba1cb3d2016-06-20 17:08:22 -0700195 // We keep the old item size so the breadcrumb will only re-render views that are necessary
196 private int mLastItemSize;
Ben Linb8c54e72016-06-10 12:13:27 -0700197
Steve McKayd9caa6a2016-09-15 16:36:45 -0700198 public BreadcrumbAdapter(com.android.documentsui.base.State state,
Ben Linba1cb3d2016-06-20 17:08:22 -0700199 Environment env,
Ben Lin340ab172017-01-27 11:41:26 -0800200 OnDragListener dragListener,
201 View.OnKeyListener clickListener) {
Ben Linb8c54e72016-06-10 12:13:27 -0700202 mState = state;
203 mEnv = env;
Ben Linba1cb3d2016-06-20 17:08:22 -0700204 mDragListener = dragListener;
Ben Lin340ab172017-01-27 11:41:26 -0800205 mClickListener = clickListener;
Ben Linba1cb3d2016-06-20 17:08:22 -0700206 mLastItemSize = mState.stack.size();
Ben Linb8c54e72016-06-10 12:13:27 -0700207 }
208
209 @Override
210 public BreadcrumbHolder onCreateViewHolder(ViewGroup parent, int viewType) {
211 View v = LayoutInflater.from(parent.getContext())
212 .inflate(R.layout.navigation_breadcrumb_item, null);
213 return new BreadcrumbHolder(v);
214 }
215
216 @Override
217 public void onBindViewHolder(BreadcrumbHolder holder, int position) {
218 final DocumentInfo doc = getItem(position);
Ben Lin9cad6352016-07-06 11:58:05 -0700219 final int horizontalPadding = (int) holder.itemView.getResources()
220 .getDimension(R.dimen.breadcrumb_item_padding);
Ben Linb8c54e72016-06-10 12:13:27 -0700221
222 if (position == 0) {
223 final RootInfo root = mEnv.getCurrentRoot();
224 holder.title.setText(root.title);
Ben Lin9cad6352016-07-06 11:58:05 -0700225 holder.title.setPadding(0, 0, horizontalPadding, 0);
Ben Linb8c54e72016-06-10 12:13:27 -0700226 } else {
227 holder.title.setText(doc.displayName);
Ben Lin9cad6352016-07-06 11:58:05 -0700228 holder.title.setPadding(horizontalPadding, 0, horizontalPadding, 0);
Ben Linb8c54e72016-06-10 12:13:27 -0700229 }
230
231 if (position == getItemCount() - 1) {
232 holder.arrow.setVisibility(View.GONE);
Ben Linba1cb3d2016-06-20 17:08:22 -0700233 } else {
234 holder.arrow.setVisibility(View.VISIBLE);
Ben Linb8c54e72016-06-10 12:13:27 -0700235 }
Ben Linba1cb3d2016-06-20 17:08:22 -0700236 holder.itemView.setOnDragListener(mDragListener);
Ben Lin340ab172017-01-27 11:41:26 -0800237 holder.itemView.setOnKeyListener(mClickListener);
Ben Linb8c54e72016-06-10 12:13:27 -0700238 }
239
240 private DocumentInfo getItem(int position) {
Garfield Tan2a837422016-10-19 11:50:45 -0700241 return mState.stack.get(position);
Ben Linb8c54e72016-06-10 12:13:27 -0700242 }
243
244 @Override
245 public int getItemCount() {
246 return mState.stack.size();
247 }
Ben Linba1cb3d2016-06-20 17:08:22 -0700248
249 public int getLastItemSize() {
250 return mLastItemSize;
251 }
252
253 public void updateLastItemSize() {
254 mLastItemSize = mState.stack.size();
255 }
Ben Linb8c54e72016-06-10 12:13:27 -0700256 }
257
258 private static class BreadcrumbHolder extends RecyclerView.ViewHolder {
259
Ben Linba1cb3d2016-06-20 17:08:22 -0700260 protected DragOverTextView title;
Ben Linb8c54e72016-06-10 12:13:27 -0700261 protected ImageView arrow;
262
263 public BreadcrumbHolder(View itemView) {
264 super(itemView);
Ben Linba1cb3d2016-06-20 17:08:22 -0700265 title = (DragOverTextView) itemView.findViewById(R.id.breadcrumb_text);
Ben Linb8c54e72016-06-10 12:13:27 -0700266 arrow = (ImageView) itemView.findViewById(R.id.breadcrumb_arrow);
267 }
Ben Linba1cb3d2016-06-20 17:08:22 -0700268
269 /**
270 * Highlights the associated item view.
271 * @param highlighted
272 */
273 public void setHighlighted(boolean highlighted) {
274 title.setHighlight(highlighted);
275 }
Ben Linb8c54e72016-06-10 12:13:27 -0700276 }
277
278 private static final class ClickListener extends GestureDetector
279 implements OnItemTouchListener {
280
281 public ClickListener(Context context, Consumer<MotionEvent> listener) {
282 super(context, new SimpleOnGestureListener() {
283 @Override
284 public boolean onSingleTapUp(MotionEvent e) {
285 listener.accept(e);
286 return true;
287 }
288 });
289 }
290
291 @Override
292 public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
293 onTouchEvent(e);
294 return false;
295 }
296
297 @Override
298 public void onTouchEvent(RecyclerView rv, MotionEvent e) {
299 onTouchEvent(e);
300 }
301
302 @Override
303 public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
304 }
305 }
306}