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