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.dirlist; 18 19 import android.graphics.Point; 20 import android.support.annotation.VisibleForTesting; 21 import android.view.DragEvent; 22 import android.view.View; 23 import android.view.View.OnDragListener; 24 25 import com.android.documentsui.ItemDragListener; 26 import com.android.documentsui.ItemDragListener.DragHost; 27 import com.android.documentsui.selection.ViewAutoScroller; 28 import com.android.documentsui.selection.ViewAutoScroller.ScrollHost; 29 import com.android.documentsui.selection.ViewAutoScroller.ScrollerCallbacks; 30 31 import java.util.function.BooleanSupplier; 32 import java.util.function.IntSupplier; 33 import java.util.function.Predicate; 34 35 import javax.annotation.Nullable; 36 37 /** 38 * This class acts as a middle-man handler for potential auto-scrolling before passing the dragEvent 39 * onto {@link DirectoryDragListener}. 40 */ 41 class DragHoverListener implements OnDragListener { 42 43 private final ItemDragListener<? extends DragHost> mDragHandler; 44 private final IntSupplier mHeight; 45 private final BooleanSupplier mCanScrollUp; 46 private final BooleanSupplier mCanScrollDown; 47 private final Runnable mDragScroller; 48 49 /** 50 * Predicate to tests whether it's the scroll view ({@link DirectoryFragment#mRecView}) itself. 51 * 52 * {@link DragHoverListener} is used for both {@link DirectoryFragment#mRecView} and its 53 * children. When we decide whether it's in the scroll zone we need to obtain the coordinate 54 * relative to {@link DirectoryFragment#mRecView} so we need to transform the coordinate if the 55 * view that gets drag and drop events is a child of {@link DirectoryFragment#mRecView}. 56 */ 57 private final Predicate<View> mIsScrollView; 58 59 private boolean mDragHappening; 60 private @Nullable Point mCurrentPosition; 61 62 @VisibleForTesting 63 DragHoverListener( 64 ItemDragListener<? extends DragHost> dragHandler, 65 IntSupplier heightSupplier, 66 Predicate<View> isScrollView, 67 BooleanSupplier scrollUpSupplier, 68 BooleanSupplier scrollDownSupplier, 69 ViewAutoScroller.ScrollerCallbacks scrollCallbacks) { 70 71 mDragHandler = dragHandler; 72 mHeight = heightSupplier; 73 mIsScrollView = isScrollView; 74 mCanScrollUp = scrollUpSupplier; 75 mCanScrollDown = scrollDownSupplier; 76 77 ScrollHost scrollHost = new ScrollHost() { 78 @Override 79 public Point getCurrentPosition() { 80 return mCurrentPosition; 81 } 82 83 @Override 84 public int getViewHeight() { 85 return mHeight.getAsInt(); 86 } 87 88 @Override 89 public boolean isActive() { 90 return mDragHappening; 91 } 92 }; 93 94 mDragScroller = new ViewAutoScroller(scrollHost, scrollCallbacks); 95 } 96 97 static DragHoverListener create( 98 ItemDragListener<? extends DragHost> dragHandler, 99 View scrollView) { 100 101 ScrollerCallbacks scrollCallbacks = new ScrollerCallbacks() { 102 @Override 103 public void scrollBy(int dy) { 104 scrollView.scrollBy(0, dy); 105 } 106 107 @Override 108 public void runAtNextFrame(Runnable r) { 109 scrollView.postOnAnimation(r); 110 111 } 112 113 @Override 114 public void removeCallback(Runnable r) { 115 scrollView.removeCallbacks(r); 116 } 117 }; 118 119 DragHoverListener listener = new DragHoverListener( 120 dragHandler, 121 scrollView::getHeight, 122 (view) -> (scrollView == view), 123 () -> scrollView.canScrollVertically(-1), 124 () -> scrollView.canScrollVertically(1), 125 scrollCallbacks); 126 127 return listener; 128 } 129 130 @Override 131 public boolean onDrag(View v, DragEvent event) { 132 switch (event.getAction()) { 133 case DragEvent.ACTION_DRAG_STARTED: 134 mDragHappening = true; 135 break; 136 case DragEvent.ACTION_DRAG_ENDED: 137 mDragHappening = false; 138 break; 139 case DragEvent.ACTION_DRAG_LOCATION: 140 handleLocationEvent(v, event.getX(), event.getY()); 141 break; 142 default: 143 break; 144 } 145 146 // Always forward events to the drag handler for item highlight, spring load, etc. 147 return mDragHandler.onDrag(v, event); 148 } 149 150 private boolean handleLocationEvent(View v, float x, float y) { 151 mCurrentPosition = transformToScrollViewCoordinate(v, x, y); 152 if (insideDragZone()) { 153 mDragScroller.run(); 154 return true; 155 } 156 return false; 157 } 158 159 private Point transformToScrollViewCoordinate(View v, float x, float y) { 160 // Check if v is the RecyclerView itself. If not we need to transform the coordinate to 161 // relative to the RecyclerView because we need to test the scroll zone in the coordinate 162 // relative to the RecyclerView; if yes we don't need to transform coordinates. 163 final boolean isScrollView = mIsScrollView.test(v); 164 final float offsetX = isScrollView ? 0 : v.getX(); 165 final float offsetY = isScrollView ? 0 : v.getY(); 166 return new Point(Math.round(offsetX + x), Math.round(offsetY + y)); 167 } 168 169 private boolean insideDragZone() { 170 if (mCurrentPosition == null) { 171 return false; 172 } 173 174 float topBottomRegionHeight = mHeight.getAsInt() 175 * ViewAutoScroller.TOP_BOTTOM_THRESHOLD_RATIO; 176 boolean shouldScrollUp = mCurrentPosition.y < topBottomRegionHeight 177 && mCanScrollUp.getAsBoolean(); 178 boolean shouldScrollDown = mCurrentPosition.y > mHeight.getAsInt() - topBottomRegionHeight 179 && mCanScrollDown.getAsBoolean(); 180 return shouldScrollUp || shouldScrollDown; 181 } 182 }