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