• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled;
20 
21 import android.content.Context;
22 import android.util.AttributeSet;
23 import android.view.GestureDetector;
24 import android.view.KeyEvent;
25 import android.view.LayoutInflater;
26 import android.view.MotionEvent;
27 import android.view.View;
28 import android.view.ViewGroup;
29 
30 import androidx.annotation.Nullable;
31 import androidx.recyclerview.widget.LinearLayoutManager;
32 import androidx.recyclerview.widget.RecyclerView;
33 
34 import com.android.documentsui.NavigationViewManager.Breadcrumb;
35 import com.android.documentsui.NavigationViewManager.Environment;
36 import com.android.documentsui.dirlist.AccessibilityEventRouter;
37 
38 import java.util.function.Consumer;
39 import java.util.function.IntConsumer;
40 
41 /**
42  * Horizontal breadcrumb
43  */
44 public final class HorizontalBreadcrumb extends RecyclerView implements Breadcrumb {
45 
46     private static final int USER_NO_SCROLL_OFFSET_THRESHOLD = 5;
47 
48     private LinearLayoutManager mLayoutManager;
49     private BreadcrumbAdapter mAdapter;
50     private IntConsumer mClickListener;
51     // Represents the top divider (border) of the breadcrumb on the compact size screen.
52     // It will be null on other screen sizes, or when the use_material3 flag is OFF.
53     private @Nullable View mTopDividerView;
54 
HorizontalBreadcrumb(Context context, AttributeSet attrs, int defStyleAttr)55     public HorizontalBreadcrumb(Context context, AttributeSet attrs, int defStyleAttr) {
56         super(context, attrs, defStyleAttr);
57     }
58 
HorizontalBreadcrumb(Context context, AttributeSet attrs)59     public HorizontalBreadcrumb(Context context, AttributeSet attrs) {
60         super(context, attrs);
61     }
62 
HorizontalBreadcrumb(Context context)63     public HorizontalBreadcrumb(Context context) {
64         super(context);
65     }
66 
67     @Override
setup(Environment env, com.android.documentsui.base.State state, IntConsumer listener, @Nullable View topDivider)68     public void setup(Environment env,
69             com.android.documentsui.base.State state,
70             IntConsumer listener,
71             @Nullable View topDivider) {
72 
73         mClickListener = listener;
74         mLayoutManager = new HorizontalBreadcrumbLinearLayoutManager(
75                 getContext(), LinearLayoutManager.HORIZONTAL, false);
76         mAdapter = new BreadcrumbAdapter(state, env, this::onKey);
77         mTopDividerView = topDivider;
78         // Since we are using GestureDetector to detect click events, a11y services don't know which
79         // views are clickable because we aren't using View.OnClickListener. Thus, we need to use a
80         // custom accessibility delegate to route click events correctly.
81         // See AccessibilityClickEventRouter for more details on how we are routing these a11y
82         // events.
83         setAccessibilityDelegateCompat(
84                 new AccessibilityEventRouter(this,
85                         (View child) -> onAccessibilityClick(child), null));
86 
87         setLayoutManager(mLayoutManager);
88         addOnItemTouchListener(new ClickListener(getContext(), this::onSingleTapUp));
89     }
90 
91     @Override
show(boolean visibility)92     public void show(boolean visibility) {
93         if (visibility) {
94             setVisibility(VISIBLE);
95             boolean shouldScroll = !hasUserDefineScrollOffset();
96             if (getAdapter() == null) {
97                 setAdapter(mAdapter);
98             } else {
99                 int currentItemCount = mAdapter.getItemCount();
100                 int lastItemCount = mAdapter.getLastItemSize();
101                 if (currentItemCount > lastItemCount) {
102                     mAdapter.notifyItemRangeInserted(lastItemCount,
103                             currentItemCount - lastItemCount);
104                     mAdapter.notifyItemChanged(lastItemCount - 1);
105                 } else if (currentItemCount < lastItemCount) {
106                     mAdapter.notifyItemRangeRemoved(currentItemCount,
107                             lastItemCount - currentItemCount);
108                     mAdapter.notifyItemChanged(currentItemCount - 1);
109                 } else {
110                     mAdapter.notifyItemChanged(currentItemCount - 1);
111                 }
112             }
113             if (shouldScroll) {
114                 mLayoutManager.scrollToPosition(mAdapter.getItemCount() - 1);
115             }
116         } else {
117             setVisibility(GONE);
118             setAdapter(null);
119         }
120         if (mTopDividerView != null) {
121             mTopDividerView.setVisibility(visibility ? VISIBLE : GONE);
122         }
123         mAdapter.updateLastItemSize();
124     }
125 
hasUserDefineScrollOffset()126     private boolean hasUserDefineScrollOffset() {
127         final int maxOffset = computeHorizontalScrollRange() - computeHorizontalScrollExtent();
128         return (maxOffset - computeHorizontalScrollOffset() > USER_NO_SCROLL_OFFSET_THRESHOLD);
129     }
130 
onAccessibilityClick(View child)131     private boolean onAccessibilityClick(View child) {
132         int pos = getChildAdapterPosition(child);
133         if (pos != getAdapter().getItemCount() - 1) {
134             mClickListener.accept(pos);
135             return true;
136         }
137         return false;
138     }
139 
onKey(View v, int keyCode, KeyEvent event)140     private boolean onKey(View v, int keyCode, KeyEvent event) {
141         switch (keyCode) {
142             case KeyEvent.KEYCODE_ENTER:
143                 return onAccessibilityClick(v);
144             default:
145                 return false;
146         }
147     }
148 
149     @Override
postUpdate()150     public void postUpdate() {
151     }
152 
onSingleTapUp(MotionEvent e)153     private void onSingleTapUp(MotionEvent e) {
154         View itemView = findChildViewUnder(e.getX(), e.getY());
155         int pos = getChildAdapterPosition(itemView);
156         if (pos != mAdapter.getItemCount() - 1 && pos != -1) {
157             mClickListener.accept(pos);
158         }
159     }
160 
161     private static final class BreadcrumbAdapter
162             extends RecyclerView.Adapter<BreadcrumbHolder> {
163 
164         private final Environment mEnv;
165         private final com.android.documentsui.base.State mState;
166         private final View.OnKeyListener mClickListener;
167         // We keep the old item size so the breadcrumb will only re-render views that are necessary
168         private int mLastItemSize;
169 
BreadcrumbAdapter(com.android.documentsui.base.State state, Environment env, View.OnKeyListener clickListener)170         public BreadcrumbAdapter(com.android.documentsui.base.State state,
171                 Environment env,
172                 View.OnKeyListener clickListener) {
173             mState = state;
174             mEnv = env;
175             mClickListener = clickListener;
176             mLastItemSize = getItemCount();
177         }
178 
179         @Override
onCreateViewHolder(ViewGroup parent, int viewType)180         public BreadcrumbHolder onCreateViewHolder(ViewGroup parent, int viewType) {
181             View v = LayoutInflater.from(parent.getContext())
182                     .inflate(R.layout.navigation_breadcrumb_item, null);
183             return new BreadcrumbHolder(v);
184         }
185 
186         @Override
onBindViewHolder(BreadcrumbHolder holder, int position)187         public void onBindViewHolder(BreadcrumbHolder holder, int position) {
188             final boolean isFirst = position == 0;
189             // Note that when isFirst is true, there might not be a DocumentInfo on the stack as it
190             // could be an error state screen accessible from the root info.
191             final boolean isLast = position == getItemCount() - 1;
192 
193             holder.mTitle.setText(
194                     isFirst ? mEnv.getCurrentRoot().title : mState.stack.get(position).displayName);
195             if (isUseMaterial3FlagEnabled()) {
196                 // The last path part in the breadcrumb is not clickable.
197                 holder.mTitle.setEnabled(!isLast);
198             } else {
199                 holder.mTitle.setEnabled(isLast);
200             }
201             if (isUseMaterial3FlagEnabled()) {
202                 final int paddingHorizontal =
203                         (int)
204                                 holder.itemView
205                                         .getResources()
206                                         .getDimension(R.dimen.breadcrumb_item_padding_horizontal);
207                 final int paddingVertical =
208                         (int)
209                                 holder.itemView
210                                         .getResources()
211                                         .getDimension(R.dimen.breadcrumb_item_padding_vertical);
212                 final int arrowPadding =
213                         (int)
214                                 holder.itemView
215                                         .getResources()
216                                         .getDimension(R.dimen.breadcrumb_item_arrow_padding);
217                 holder.mTitle.setPadding(
218                         paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical);
219 
220                 ViewGroup.MarginLayoutParams params =
221                         (ViewGroup.MarginLayoutParams) holder.mArrow.getLayoutParams();
222                 params.setMarginStart(arrowPadding);
223                 params.setMarginEnd(arrowPadding);
224                 holder.mArrow.setLayoutParams(params);
225             } else {
226                 final int padding = (int) holder.itemView.getResources()
227                         .getDimension(R.dimen.breadcrumb_item_padding);
228                 holder.mTitle.setPadding(
229                         isFirst ? padding * 3 : padding,
230                         padding,
231                         isLast ? padding * 2 : padding,
232                         padding);
233             }
234             holder.mArrow.setVisibility(isLast ? View.GONE : View.VISIBLE);
235 
236             holder.itemView.setOnKeyListener(mClickListener);
237             holder.setLast(isLast);
238         }
239 
240         @Override
getItemCount()241         public int getItemCount() {
242             // Don't show recents in the breadcrumb.
243             if (mState.stack.isRecents()) {
244                 return 0;
245             }
246             // Continue showing the root title in the breadcrumb for cross-profile error screens.
247             if (mState.supportsCrossProfile()
248                     && mState.stack.size() == 0
249                     && mState.stack.getRoot() != null
250                     && mState.stack.getRoot().supportsCrossProfile()) {
251                 return 1;
252             }
253             return mState.stack.size();
254         }
255 
getLastItemSize()256         public int getLastItemSize() {
257             return mLastItemSize;
258         }
259 
updateLastItemSize()260         public void updateLastItemSize() {
261             mLastItemSize = getItemCount();
262         }
263     }
264 
265     private static final class ClickListener extends GestureDetector
266             implements OnItemTouchListener {
267 
ClickListener(Context context, Consumer<MotionEvent> listener)268         public ClickListener(Context context, Consumer<MotionEvent> listener) {
269             super(context, new SimpleOnGestureListener() {
270                 @Override
271                 public boolean onSingleTapUp(MotionEvent e) {
272                     listener.accept(e);
273                     return true;
274                 }
275             });
276         }
277 
278         @Override
onInterceptTouchEvent(RecyclerView rv, MotionEvent e)279         public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
280             onTouchEvent(e);
281             return false;
282         }
283 
284         @Override
onTouchEvent(RecyclerView rv, MotionEvent e)285         public void onTouchEvent(RecyclerView rv, MotionEvent e) {
286             onTouchEvent(e);
287         }
288 
289         @Override
onRequestDisallowInterceptTouchEvent(boolean disallowIntercept)290         public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
291         }
292     }
293 
294     private static class HorizontalBreadcrumbLinearLayoutManager extends LinearLayoutManager {
295 
296         /**
297          * Disable predictive animations. There is a bug in RecyclerView which causes views that
298          * are being reloaded to pull invalid view holders from the internal recycler stack if the
299          * adapter size has decreased since the ViewHolder was recycled.
300          */
301         @Override
supportsPredictiveItemAnimations()302         public boolean supportsPredictiveItemAnimations() {
303             return false;
304         }
305 
HorizontalBreadcrumbLinearLayoutManager( Context context, int orientation, boolean reverseLayout)306         HorizontalBreadcrumbLinearLayoutManager(
307                 Context context, int orientation, boolean reverseLayout) {
308             super(context, orientation, reverseLayout);
309         }
310     }
311 }
312