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