Home | History | Annotate | Download | only in documentsui
      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 
     52     public HorizontalBreadcrumb(Context context, AttributeSet attrs, int defStyleAttr) {
     53         super(context, attrs, defStyleAttr);
     54     }
     55 
     56     public HorizontalBreadcrumb(Context context, AttributeSet attrs) {
     57         super(context, attrs);
     58     }
     59 
     60     public HorizontalBreadcrumb(Context context) {
     61         super(context);
     62     }
     63 
     64     @Override
     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
     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 
    116     private boolean hasUserDefineScrollOffset() {
    117         final int maxOffset = computeHorizontalScrollRange() - computeHorizontalScrollExtent();
    118         return (maxOffset - computeHorizontalScrollOffset() > USER_NO_SCROLL_OFFSET_THRESHOLD);
    119     }
    120 
    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 
    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
    140     public void postUpdate() {
    141     }
    142 
    143     @Override
    144     public void runOnUiThread(Runnable runnable) {
    145         post(runnable);
    146     }
    147 
    148     @Override
    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
    157     public void onDragEntered(View v) {
    158         // do nothing
    159     }
    160 
    161     @Override
    162     public void onDragExited(View v) {
    163         // do nothing
    164     }
    165 
    166     @Override
    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
    175     public void onDragEnded() {
    176         // do nothing
    177     }
    178 
    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 
    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
    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
    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 
    239         private DocumentInfo getItem(int position) {
    240             return mState.stack.get(position);
    241         }
    242 
    243         @Override
    244         public int getItemCount() {
    245             return mState.stack.size();
    246         }
    247 
    248         public int getLastItemSize() {
    249             return mLastItemSize;
    250         }
    251 
    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 
    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          */
    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 
    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
    291         public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
    292             onTouchEvent(e);
    293             return false;
    294         }
    295 
    296         @Override
    297         public void onTouchEvent(RecyclerView rv, MotionEvent e) {
    298             onTouchEvent(e);
    299         }
    300 
    301         @Override
    302         public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    303         }
    304     }
    305 }
    306