blob: 91480dbb580dabe2686d14ed44f6c2ccae3a18ca [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(
Tony Huangf6bf0052019-08-07 18:06:50 +080074 state, env, new ItemDragListener<>(this), this::onKey,
75 new AccessibilityEventRouter(this::onAccessibilityClick, null));
Ben Linb8c54e72016-06-10 12:13:27 -070076
77 setLayoutManager(mLayoutManager);
78 addOnItemTouchListener(new ClickListener(getContext(), this::onSingleTapUp));
79 }
80
81 @Override
82 public void show(boolean visibility) {
83 if (visibility) {
84 setVisibility(VISIBLE);
Ben Linba1cb3d2016-06-20 17:08:22 -070085 boolean shouldScroll = !hasUserDefineScrollOffset();
86 if (getAdapter() == null) {
87 setAdapter(mAdapter);
88 } else {
89 int currentItemCount = mAdapter.getItemCount();
90 int lastItemCount = mAdapter.getLastItemSize();
91 if (currentItemCount > lastItemCount) {
92 mAdapter.notifyItemRangeInserted(lastItemCount,
93 currentItemCount - lastItemCount);
94 mAdapter.notifyItemChanged(lastItemCount - 1);
95 } else if (currentItemCount < lastItemCount) {
96 mAdapter.notifyItemRangeRemoved(currentItemCount,
97 lastItemCount - currentItemCount);
98 mAdapter.notifyItemChanged(currentItemCount - 1);
99 }
100 }
101 if (shouldScroll) {
102 mLayoutManager.scrollToPosition(mAdapter.getItemCount() - 1);
103 }
Ben Linb8c54e72016-06-10 12:13:27 -0700104 } else {
105 setVisibility(GONE);
106 setAdapter(null);
107 }
Ben Linba1cb3d2016-06-20 17:08:22 -0700108 mAdapter.updateLastItemSize();
109 }
110
111 private boolean hasUserDefineScrollOffset() {
112 final int maxOffset = computeHorizontalScrollRange() - computeHorizontalScrollExtent();
113 return (maxOffset - computeHorizontalScrollOffset() > USER_NO_SCROLL_OFFSET_THRESHOLD);
Ben Linb8c54e72016-06-10 12:13:27 -0700114 }
115
Ben Line4ffd062017-01-19 15:10:49 -0800116 private boolean onAccessibilityClick(View child) {
117 int pos = getChildAdapterPosition(child);
118 if (pos != getAdapter().getItemCount() - 1) {
119 mClickListener.accept(pos);
120 return true;
121 }
122 return false;
123 }
124
Ben Lin340ab172017-01-27 11:41:26 -0800125 private boolean onKey(View v, int keyCode, KeyEvent event) {
126 switch (keyCode) {
127 case KeyEvent.KEYCODE_ENTER:
128 return onAccessibilityClick(v);
129 default:
130 return false;
131 }
132 }
133
Ben Linb8c54e72016-06-10 12:13:27 -0700134 @Override
135 public void postUpdate() {
136 }
137
Ben Linba1cb3d2016-06-20 17:08:22 -0700138 @Override
139 public void runOnUiThread(Runnable runnable) {
140 post(runnable);
141 }
142
143 @Override
Garfield Tanda2c0f02017-04-11 13:47:58 -0700144 public void setDropTargetHighlight(View v, boolean highlight) {
Ben Linba1cb3d2016-06-20 17:08:22 -0700145 RecyclerView.ViewHolder vh = getChildViewHolder(v);
146 if (vh instanceof BreadcrumbHolder) {
147 ((BreadcrumbHolder) vh).setHighlighted(highlight);
148 }
149 }
150
151 @Override
Garfield Tanda2c0f02017-04-11 13:47:58 -0700152 public void onDragEntered(View v) {
Ben Lin7f72a3c2016-09-27 16:37:28 -0700153 // do nothing
154 }
155
156 @Override
Garfield Tanda2c0f02017-04-11 13:47:58 -0700157 public void onDragExited(View v) {
Ben Lind0202122016-11-10 18:00:12 -0800158 // do nothing
159 }
160
161 @Override
Ben Linba1cb3d2016-06-20 17:08:22 -0700162 public void onViewHovered(View v) {
163 int pos = getChildAdapterPosition(v);
164 if (pos != mAdapter.getItemCount() - 1) {
Ben Line4ffd062017-01-19 15:10:49 -0800165 mClickListener.accept(pos);
Ben Linba1cb3d2016-06-20 17:08:22 -0700166 }
167 }
168
Garfield Tanda2c0f02017-04-11 13:47:58 -0700169 @Override
170 public void onDragEnded() {
171 // do nothing
172 }
173
Ben Linb8c54e72016-06-10 12:13:27 -0700174 private void onSingleTapUp(MotionEvent e) {
175 View itemView = findChildViewUnder(e.getX(), e.getY());
176 int pos = getChildAdapterPosition(itemView);
177 if (pos != mAdapter.getItemCount() - 1) {
Ben Line4ffd062017-01-19 15:10:49 -0800178 mClickListener.accept(pos);
Ben Linb8c54e72016-06-10 12:13:27 -0700179 }
180 }
181
182 private static final class BreadcrumbAdapter
183 extends RecyclerView.Adapter<BreadcrumbHolder> {
184
Ben Linba1cb3d2016-06-20 17:08:22 -0700185 private final Environment mEnv;
Steve McKayd9caa6a2016-09-15 16:36:45 -0700186 private final com.android.documentsui.base.State mState;
Ben Linba1cb3d2016-06-20 17:08:22 -0700187 private final OnDragListener mDragListener;
Ben Lin340ab172017-01-27 11:41:26 -0800188 private final View.OnKeyListener mClickListener;
Tony Huangf6bf0052019-08-07 18:06:50 +0800189 private final View.AccessibilityDelegate mAccessibilityDelegate;
Ben Linba1cb3d2016-06-20 17:08:22 -0700190 // We keep the old item size so the breadcrumb will only re-render views that are necessary
191 private int mLastItemSize;
Ben Linb8c54e72016-06-10 12:13:27 -0700192
Steve McKayd9caa6a2016-09-15 16:36:45 -0700193 public BreadcrumbAdapter(com.android.documentsui.base.State state,
Ben Linba1cb3d2016-06-20 17:08:22 -0700194 Environment env,
Ben Lin340ab172017-01-27 11:41:26 -0800195 OnDragListener dragListener,
Tony Huangf6bf0052019-08-07 18:06:50 +0800196 View.OnKeyListener clickListener,
197 View.AccessibilityDelegate accessibilityDelegate) {
Ben Linb8c54e72016-06-10 12:13:27 -0700198 mState = state;
199 mEnv = env;
Ben Linba1cb3d2016-06-20 17:08:22 -0700200 mDragListener = dragListener;
Ben Lin340ab172017-01-27 11:41:26 -0800201 mClickListener = clickListener;
Tony Huangf6bf0052019-08-07 18:06:50 +0800202 mAccessibilityDelegate = accessibilityDelegate;
Ben Linba1cb3d2016-06-20 17:08:22 -0700203 mLastItemSize = mState.stack.size();
Ben Linb8c54e72016-06-10 12:13:27 -0700204 }
205
206 @Override
207 public BreadcrumbHolder onCreateViewHolder(ViewGroup parent, int viewType) {
208 View v = LayoutInflater.from(parent.getContext())
209 .inflate(R.layout.navigation_breadcrumb_item, null);
210 return new BreadcrumbHolder(v);
211 }
212
213 @Override
214 public void onBindViewHolder(BreadcrumbHolder holder, int position) {
215 final DocumentInfo doc = getItem(position);
Ben Lin9cad6352016-07-06 11:58:05 -0700216 final int horizontalPadding = (int) holder.itemView.getResources()
217 .getDimension(R.dimen.breadcrumb_item_padding);
Ben Linb8c54e72016-06-10 12:13:27 -0700218
219 if (position == 0) {
220 final RootInfo root = mEnv.getCurrentRoot();
221 holder.title.setText(root.title);
Ben Lin9cad6352016-07-06 11:58:05 -0700222 holder.title.setPadding(0, 0, horizontalPadding, 0);
Ben Linb8c54e72016-06-10 12:13:27 -0700223 } else {
224 holder.title.setText(doc.displayName);
Ben Lin9cad6352016-07-06 11:58:05 -0700225 holder.title.setPadding(horizontalPadding, 0, horizontalPadding, 0);
Ben Linb8c54e72016-06-10 12:13:27 -0700226 }
227
228 if (position == getItemCount() - 1) {
229 holder.arrow.setVisibility(View.GONE);
Ben Linba1cb3d2016-06-20 17:08:22 -0700230 } else {
231 holder.arrow.setVisibility(View.VISIBLE);
Ben Linb8c54e72016-06-10 12:13:27 -0700232 }
Ben Linba1cb3d2016-06-20 17:08:22 -0700233 holder.itemView.setOnDragListener(mDragListener);
Ben Lin340ab172017-01-27 11:41:26 -0800234 holder.itemView.setOnKeyListener(mClickListener);
Tony Huangf6bf0052019-08-07 18:06:50 +0800235 // Since we are using GestureDetector to detect click events, a11y services don't know
236 // which views are clickable because we aren't using View.OnClickListener. Thus, we
237 // need to use a custom accessibility delegate to route click events correctly.
238 // See AccessibilityClickEventRouter for more details on how we are routing these
239 // a11y events.
240 holder.itemView.setAccessibilityDelegate(mAccessibilityDelegate);
Ben Linb8c54e72016-06-10 12:13:27 -0700241 }
242
243 private DocumentInfo getItem(int position) {
Garfield Tan2a837422016-10-19 11:50:45 -0700244 return mState.stack.get(position);
Ben Linb8c54e72016-06-10 12:13:27 -0700245 }
246
247 @Override
248 public int getItemCount() {
249 return mState.stack.size();
250 }
Ben Linba1cb3d2016-06-20 17:08:22 -0700251
252 public int getLastItemSize() {
253 return mLastItemSize;
254 }
255
256 public void updateLastItemSize() {
257 mLastItemSize = mState.stack.size();
258 }
Ben Linb8c54e72016-06-10 12:13:27 -0700259 }
260
261 private static class BreadcrumbHolder extends RecyclerView.ViewHolder {
262
Ben Linba1cb3d2016-06-20 17:08:22 -0700263 protected DragOverTextView title;
Ben Linb8c54e72016-06-10 12:13:27 -0700264 protected ImageView arrow;
265
266 public BreadcrumbHolder(View itemView) {
267 super(itemView);
Ben Linba1cb3d2016-06-20 17:08:22 -0700268 title = (DragOverTextView) itemView.findViewById(R.id.breadcrumb_text);
Ben Linb8c54e72016-06-10 12:13:27 -0700269 arrow = (ImageView) itemView.findViewById(R.id.breadcrumb_arrow);
270 }
Ben Linba1cb3d2016-06-20 17:08:22 -0700271
272 /**
273 * Highlights the associated item view.
274 * @param highlighted
275 */
276 public void setHighlighted(boolean highlighted) {
277 title.setHighlight(highlighted);
278 }
Ben Linb8c54e72016-06-10 12:13:27 -0700279 }
280
281 private static final class ClickListener extends GestureDetector
282 implements OnItemTouchListener {
283
284 public ClickListener(Context context, Consumer<MotionEvent> listener) {
285 super(context, new SimpleOnGestureListener() {
286 @Override
287 public boolean onSingleTapUp(MotionEvent e) {
288 listener.accept(e);
289 return true;
290 }
291 });
292 }
293
294 @Override
295 public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
296 onTouchEvent(e);
297 return false;
298 }
299
300 @Override
301 public void onTouchEvent(RecyclerView rv, MotionEvent e) {
302 onTouchEvent(e);
303 }
304
305 @Override
306 public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
307 }
308 }
309}