• 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, Object localState, boolean highlight)149     public void setDropTargetHighlight(View v, Object localState, 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, Object localState)157     public void onDragEntered(View v, Object localState) {
158         // do nothing
159     }
160 
161     @Override
onDragExited(View v, Object localState)162     public void onDragExited(View v, Object localState) {
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 
onSingleTapUp(MotionEvent e)174     private void onSingleTapUp(MotionEvent e) {
175         View itemView = findChildViewUnder(e.getX(), e.getY());
176         int pos = getChildAdapterPosition(itemView);
177         if (pos != mAdapter.getItemCount() - 1) {
178             mClickListener.accept(pos);
179         }
180     }
181 
182     private static final class BreadcrumbAdapter
183             extends RecyclerView.Adapter<BreadcrumbHolder> {
184 
185         private final Environment mEnv;
186         private final com.android.documentsui.base.State mState;
187         private final OnDragListener mDragListener;
188         private final View.OnKeyListener mClickListener;
189         // We keep the old item size so the breadcrumb will only re-render views that are necessary
190         private int mLastItemSize;
191 
BreadcrumbAdapter(com.android.documentsui.base.State state, Environment env, OnDragListener dragListener, View.OnKeyListener clickListener)192         public BreadcrumbAdapter(com.android.documentsui.base.State state,
193                 Environment env,
194                 OnDragListener dragListener,
195                 View.OnKeyListener clickListener) {
196             mState = state;
197             mEnv = env;
198             mDragListener = dragListener;
199             mClickListener = clickListener;
200             mLastItemSize = mState.stack.size();
201         }
202 
203         @Override
onCreateViewHolder(ViewGroup parent, int viewType)204         public BreadcrumbHolder onCreateViewHolder(ViewGroup parent, int viewType) {
205             View v = LayoutInflater.from(parent.getContext())
206                     .inflate(R.layout.navigation_breadcrumb_item, null);
207             return new BreadcrumbHolder(v);
208         }
209 
210         @Override
onBindViewHolder(BreadcrumbHolder holder, int position)211         public void onBindViewHolder(BreadcrumbHolder holder, int position) {
212             final DocumentInfo doc = getItem(position);
213             final int horizontalPadding = (int) holder.itemView.getResources()
214                     .getDimension(R.dimen.breadcrumb_item_padding);
215 
216             if (position == 0) {
217                 final RootInfo root = mEnv.getCurrentRoot();
218                 holder.title.setText(root.title);
219                 holder.title.setPadding(0, 0, horizontalPadding, 0);
220             } else {
221                 holder.title.setText(doc.displayName);
222                 holder.title.setPadding(horizontalPadding, 0, horizontalPadding, 0);
223             }
224 
225             if (position == getItemCount() - 1) {
226                 holder.arrow.setVisibility(View.GONE);
227             } else {
228                 holder.arrow.setVisibility(View.VISIBLE);
229             }
230             holder.itemView.setOnDragListener(mDragListener);
231             holder.itemView.setOnKeyListener(mClickListener);
232         }
233 
getItem(int position)234         private DocumentInfo getItem(int position) {
235             return mState.stack.get(position);
236         }
237 
238         @Override
getItemCount()239         public int getItemCount() {
240             return mState.stack.size();
241         }
242 
getLastItemSize()243         public int getLastItemSize() {
244             return mLastItemSize;
245         }
246 
updateLastItemSize()247         public void updateLastItemSize() {
248             mLastItemSize = mState.stack.size();
249         }
250     }
251 
252     private static class BreadcrumbHolder extends RecyclerView.ViewHolder {
253 
254         protected DragOverTextView title;
255         protected ImageView arrow;
256 
BreadcrumbHolder(View itemView)257         public BreadcrumbHolder(View itemView) {
258             super(itemView);
259             title = (DragOverTextView) itemView.findViewById(R.id.breadcrumb_text);
260             arrow = (ImageView) itemView.findViewById(R.id.breadcrumb_arrow);
261         }
262 
263         /**
264          * Highlights the associated item view.
265          * @param highlighted
266          */
setHighlighted(boolean highlighted)267         public void setHighlighted(boolean highlighted) {
268             title.setHighlight(highlighted);
269         }
270     }
271 
272     private static final class ClickListener extends GestureDetector
273             implements OnItemTouchListener {
274 
ClickListener(Context context, Consumer<MotionEvent> listener)275         public ClickListener(Context context, Consumer<MotionEvent> listener) {
276             super(context, new SimpleOnGestureListener() {
277                 @Override
278                 public boolean onSingleTapUp(MotionEvent e) {
279                     listener.accept(e);
280                     return true;
281                 }
282             });
283         }
284 
285         @Override
onInterceptTouchEvent(RecyclerView rv, MotionEvent e)286         public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
287             onTouchEvent(e);
288             return false;
289         }
290 
291         @Override
onTouchEvent(RecyclerView rv, MotionEvent e)292         public void onTouchEvent(RecyclerView rv, MotionEvent e) {
293             onTouchEvent(e);
294         }
295 
296         @Override
onRequestDisallowInterceptTouchEvent(boolean disallowIntercept)297         public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
298         }
299     }
300 }
301