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