1 /*
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 
17 package com.android.documentsui;
18 
19 import android.content.Context;
20 import android.util.AttributeSet;
21 import android.view.GestureDetector;
22 import android.view.KeyEvent;
23 import android.view.LayoutInflater;
24 import android.view.MotionEvent;
25 import android.view.View;
26 import android.view.ViewGroup;
27 import android.widget.ImageView;
28 
29 import androidx.recyclerview.widget.LinearLayoutManager;
30 import androidx.recyclerview.widget.RecyclerView;
31 
32 import com.android.documentsui.NavigationViewManager.Breadcrumb;
33 import com.android.documentsui.NavigationViewManager.Environment;
34 import com.android.documentsui.base.DocumentInfo;
35 import com.android.documentsui.base.RootInfo;
36 import com.android.documentsui.dirlist.AccessibilityEventRouter;
37 
38 import java.util.function.Consumer;
39 import java.util.function.IntConsumer;
40 
41 /**
42  * Horizontal implementation of breadcrumb used for tablet / desktop device layouts
43  */
44 public final class HorizontalBreadcrumb extends RecyclerView
45         implements Breadcrumb, ItemDragListener.DragHost {
46 
47     private static final int USER_NO_SCROLL_OFFSET_THRESHOLD = 5;
48 
49     private LinearLayoutManager mLayoutManager;
50     private BreadcrumbAdapter mAdapter;
51     private IntConsumer mClickListener;
52 
HorizontalBreadcrumb(Context context, AttributeSet attrs, int defStyleAttr)53     public HorizontalBreadcrumb(Context context, AttributeSet attrs, int defStyleAttr) {
54         super(context, attrs, defStyleAttr);
55     }
56 
HorizontalBreadcrumb(Context context, AttributeSet attrs)57     public HorizontalBreadcrumb(Context context, AttributeSet attrs) {
58         super(context, attrs);
59     }
60 
HorizontalBreadcrumb(Context context)61     public HorizontalBreadcrumb(Context context) {
62         super(context);
63     }
64 
65     @Override
setup(Environment env, com.android.documentsui.base.State state, IntConsumer listener)66     public void setup(Environment env,
67             com.android.documentsui.base.State state,
68             IntConsumer listener) {
69 
70         mClickListener = listener;
71         mLayoutManager = new LinearLayoutManager(
72                 getContext(), LinearLayoutManager.HORIZONTAL, false);
73         mAdapter = new BreadcrumbAdapter(
74                 state, env, new ItemDragListener<>(this), this::onKey);
75         // 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(
80                 new AccessibilityEventRouter(this,
81                         (View child) -> onAccessibilityClick(child), null));
82 
83         setLayoutManager(mLayoutManager);
84         addOnItemTouchListener(new ClickListener(getContext(), this::onSingleTapUp));
85     }
86 
87     @Override
show(boolean visibility)88     public void show(boolean visibility) {
89         if (visibility) {
90             setVisibility(VISIBLE);
91             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             }
110         } else {
111             setVisibility(GONE);
112             setAdapter(null);
113         }
114         mAdapter.updateLastItemSize();
115     }
116 
hasUserDefineScrollOffset()117     private boolean hasUserDefineScrollOffset() {
118         final int maxOffset = computeHorizontalScrollRange() - computeHorizontalScrollExtent();
119         return (maxOffset - computeHorizontalScrollOffset() > USER_NO_SCROLL_OFFSET_THRESHOLD);
120     }
121 
onAccessibilityClick(View child)122     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 
onKey(View v, int keyCode, KeyEvent event)131     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 
140     @Override
postUpdate()141     public void postUpdate() {
142     }
143 
144     @Override
runOnUiThread(Runnable runnable)145     public void runOnUiThread(Runnable runnable) {
146         post(runnable);
147     }
148 
149     @Override
setDropTargetHighlight(View v, boolean highlight)150     public void setDropTargetHighlight(View v, boolean highlight) {
151         RecyclerView.ViewHolder vh = getChildViewHolder(v);
152         if (vh instanceof BreadcrumbHolder) {
153             ((BreadcrumbHolder) vh).setHighlighted(highlight);
154         }
155     }
156 
157     @Override
onDragEntered(View v)158     public void onDragEntered(View v) {
159         // do nothing
160     }
161 
162     @Override
onDragExited(View v)163     public void onDragExited(View v) {
164         // do nothing
165     }
166 
167     @Override
onViewHovered(View v)168     public void onViewHovered(View v) {
169         int pos = getChildAdapterPosition(v);
170         if (pos != mAdapter.getItemCount() - 1) {
171             mClickListener.accept(pos);
172         }
173     }
174 
175     @Override
onDragEnded()176     public void onDragEnded() {
177         // do nothing
178     }
179 
onSingleTapUp(MotionEvent e)180     private void onSingleTapUp(MotionEvent e) {
181         View itemView = findChildViewUnder(e.getX(), e.getY());
182         int pos = getChildAdapterPosition(itemView);
183         if (pos != mAdapter.getItemCount() - 1) {
184             mClickListener.accept(pos);
185         }
186     }
187 
188     private static final class BreadcrumbAdapter
189             extends RecyclerView.Adapter<BreadcrumbHolder> {
190 
191         private final Environment mEnv;
192         private final com.android.documentsui.base.State mState;
193         private final OnDragListener mDragListener;
194         private final View.OnKeyListener mClickListener;
195         // We keep the old item size so the breadcrumb will only re-render views that are necessary
196         private int mLastItemSize;
197 
BreadcrumbAdapter(com.android.documentsui.base.State state, Environment env, OnDragListener dragListener, View.OnKeyListener clickListener)198         public BreadcrumbAdapter(com.android.documentsui.base.State state,
199                 Environment env,
200                 OnDragListener dragListener,
201                 View.OnKeyListener clickListener) {
202             mState = state;
203             mEnv = env;
204             mDragListener = dragListener;
205             mClickListener = clickListener;
206             mLastItemSize = mState.stack.size();
207         }
208 
209         @Override
onCreateViewHolder(ViewGroup parent, int viewType)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
onBindViewHolder(BreadcrumbHolder holder, int position)217         public void onBindViewHolder(BreadcrumbHolder holder, int position) {
218             final DocumentInfo doc = getItem(position);
219             final int horizontalPadding = (int) holder.itemView.getResources()
220                     .getDimension(R.dimen.breadcrumb_item_padding);
221 
222             if (position == 0) {
223                 final RootInfo root = mEnv.getCurrentRoot();
224                 holder.title.setText(root.title);
225                 holder.title.setPadding(0, 0, horizontalPadding, 0);
226             } else {
227                 holder.title.setText(doc.displayName);
228                 holder.title.setPadding(horizontalPadding, 0, horizontalPadding, 0);
229             }
230 
231             if (position == getItemCount() - 1) {
232                 holder.arrow.setVisibility(View.GONE);
233             } else {
234                 holder.arrow.setVisibility(View.VISIBLE);
235             }
236             holder.itemView.setOnDragListener(mDragListener);
237             holder.itemView.setOnKeyListener(mClickListener);
238         }
239 
getItem(int position)240         private DocumentInfo getItem(int position) {
241             return mState.stack.get(position);
242         }
243 
244         @Override
getItemCount()245         public int getItemCount() {
246             return mState.stack.size();
247         }
248 
getLastItemSize()249         public int getLastItemSize() {
250             return mLastItemSize;
251         }
252 
updateLastItemSize()253         public void updateLastItemSize() {
254             mLastItemSize = mState.stack.size();
255         }
256     }
257 
258     private static class BreadcrumbHolder extends RecyclerView.ViewHolder {
259 
260         protected DragOverTextView title;
261         protected ImageView arrow;
262 
BreadcrumbHolder(View itemView)263         public BreadcrumbHolder(View itemView) {
264             super(itemView);
265             title = (DragOverTextView) itemView.findViewById(R.id.breadcrumb_text);
266             arrow = (ImageView) itemView.findViewById(R.id.breadcrumb_arrow);
267         }
268 
269         /**
270          * Highlights the associated item view.
271          * @param highlighted
272          */
setHighlighted(boolean highlighted)273         public void setHighlighted(boolean highlighted) {
274             title.setHighlight(highlighted);
275         }
276     }
277 
278     private static final class ClickListener extends GestureDetector
279             implements OnItemTouchListener {
280 
ClickListener(Context context, Consumer<MotionEvent> listener)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
onInterceptTouchEvent(RecyclerView rv, MotionEvent e)292         public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
293             onTouchEvent(e);
294             return false;
295         }
296 
297         @Override
onTouchEvent(RecyclerView rv, MotionEvent e)298         public void onTouchEvent(RecyclerView rv, MotionEvent e) {
299             onTouchEvent(e);
300         }
301 
302         @Override
onRequestDisallowInterceptTouchEvent(boolean disallowIntercept)303         public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
304         }
305     }
306 }
307