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.ui.ViewAutoScroller; 28 import com.android.documentsui.ui.ViewAutoScroller.ScrollActionDelegate; 29 import com.android.documentsui.ui.ViewAutoScroller.ScrollDistanceDelegate; 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.ScrollActionDelegate actionDelegate) { 70 mDragHandler = dragHandler; 71 mHeight = heightSupplier; 72 mIsScrollView = isScrollView; 73 mCanScrollUp = scrollUpSupplier; 74 mCanScrollDown = scrollDownSupplier; 75 76 ScrollDistanceDelegate distanceDelegate = new ScrollDistanceDelegate() { 77 @Override 78 public Point getCurrentPosition() { 79 return mCurrentPosition; 80 } 81 82 @Override 83 public int getViewHeight() { 84 return mHeight.getAsInt(); 85 } 86 87 @Override 88 public boolean isActive() { 89 return mDragHappening; 90 } 91 }; 92 93 mDragScroller = new ViewAutoScroller(distanceDelegate, actionDelegate); 94 } 95 96 static DragHoverListener create( 97 ItemDragListener<? extends DragHost> dragHandler, 98 View scrollView) { 99 ScrollActionDelegate actionDelegate = new ScrollActionDelegate() { 100 @Override 101 public void scrollBy(int dy) { 102 scrollView.scrollBy(0, dy); 103 } 104 105 @Override 106 public void runAtNextFrame(Runnable r) { 107 scrollView.postOnAnimation(r); 108 109 } 110 111 @Override 112 public void removeCallback(Runnable r) { 113 scrollView.removeCallbacks(r); 114 } 115 }; 116 DragHoverListener listener = new DragHoverListener( 117 dragHandler, 118 scrollView::getHeight, 119 (view) -> (scrollView == view), 120 () -> scrollView.canScrollVertically(-1), 121 () -> scrollView.canScrollVertically(1), 122 actionDelegate); 123 return listener; 124 } 125 126 @Override 127 public boolean onDrag(View v, DragEvent event) { 128 switch (event.getAction()) { 129 case DragEvent.ACTION_DRAG_STARTED: 130 mDragHappening = true; 131 break; 132 case DragEvent.ACTION_DRAG_ENDED: 133 mDragHappening = false; 134 break; 135 case DragEvent.ACTION_DRAG_LOCATION: 136 handleLocationEvent(v, event.getX(), event.getY()); 137 break; 138 default: 139 break; 140 } 141 142 // Always forward events to the drag handler for item highlight, spring load, etc. 143 return mDragHandler.onDrag(v, event); 144 } 145 146 private boolean handleLocationEvent(View v, float x, float y) { 147 mCurrentPosition = transformToScrollViewCoordinate(v, x, y); 148 if (insideDragZone()) { 149 mDragScroller.run(); 150 return true; 151 } 152 return false; 153 } 154 155 private Point transformToScrollViewCoordinate(View v, float x, float y) { 156 // Check if v is the RecyclerView itself. If not we need to transform the coordinate to 157 // relative to the RecyclerView because we need to test the scroll zone in the coordinate 158 // relative to the RecyclerView; if yes we don't need to transform coordinates. 159 final boolean isScrollView = mIsScrollView.test(v); 160 final float offsetX = isScrollView ? 0 : v.getX(); 161 final float offsetY = isScrollView ? 0 : v.getY(); 162 return new Point(Math.round(offsetX + x), Math.round(offsetY + y)); 163 } 164 165 private boolean insideDragZone() { 166 if (mCurrentPosition == null) { 167 return false; 168 } 169 170 float topBottomRegionHeight = mHeight.getAsInt() 171 * ViewAutoScroller.TOP_BOTTOM_THRESHOLD_RATIO; 172 boolean shouldScrollUp = mCurrentPosition.y < topBottomRegionHeight 173 && mCanScrollUp.getAsBoolean(); 174 boolean shouldScrollDown = mCurrentPosition.y > mHeight.getAsInt() - topBottomRegionHeight 175 && mCanScrollDown.getAsBoolean(); 176 return shouldScrollUp || shouldScrollDown; 177 } 178 }