1 /* 2 * Copyright (C) 2020 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.internal.view; 18 19 import android.annotation.NonNull; 20 import android.graphics.Point; 21 import android.graphics.Rect; 22 import android.os.CancellationSignal; 23 import android.view.View; 24 import android.view.ViewGroup; 25 import android.view.ViewParent; 26 27 import java.util.function.Consumer; 28 29 /** 30 * ScrollCapture for ScrollView and <i>ScrollView-like</i> ViewGroups. 31 * <p> 32 * Requirements for proper operation: 33 * <ul> 34 * <li>contains at most 1 child. 35 * <li>scrolls to absolute positions with {@link View#scrollTo(int, int)}. 36 * <li>has a finite, known content height and scrolling range 37 * <li>correctly implements {@link View#canScrollVertically(int)} 38 * <li>correctly implements {@link ViewParent#requestChildRectangleOnScreen(View, 39 * Rect, boolean)} 40 * </ul> 41 * 42 * @see ScrollCaptureViewSupport 43 */ 44 public class ScrollViewCaptureHelper implements ScrollCaptureViewHelper<ViewGroup> { 45 private int mStartScrollY; 46 private boolean mScrollBarEnabled; 47 private int mOverScrollMode; 48 onAcceptSession(@onNull ViewGroup view)49 public boolean onAcceptSession(@NonNull ViewGroup view) { 50 return view.isVisibleToUser() 51 && (view.canScrollVertically(UP) || view.canScrollVertically(DOWN)); 52 } 53 onPrepareForStart(@onNull ViewGroup view, Rect scrollBounds)54 public void onPrepareForStart(@NonNull ViewGroup view, Rect scrollBounds) { 55 mStartScrollY = view.getScrollY(); 56 mOverScrollMode = view.getOverScrollMode(); 57 if (mOverScrollMode != View.OVER_SCROLL_NEVER) { 58 view.setOverScrollMode(View.OVER_SCROLL_NEVER); 59 } 60 mScrollBarEnabled = view.isVerticalScrollBarEnabled(); 61 if (mScrollBarEnabled) { 62 view.setVerticalScrollBarEnabled(false); 63 } 64 } 65 onScrollRequested(@onNull ViewGroup view, Rect scrollBounds, Rect requestRect, CancellationSignal signal, Consumer<ScrollResult> resultConsumer)66 public void onScrollRequested(@NonNull ViewGroup view, Rect scrollBounds, 67 Rect requestRect, CancellationSignal signal, Consumer<ScrollResult> resultConsumer) { 68 /* 69 +---------+ <----+ Content [25,25 - 275,1025] (w=250,h=1000) 70 | | 71 ...|.........|... startScrollY=100 72 | | 73 +--+---------+---+ <--+ Container View [0,0 - 300,300] (scrollY=200) 74 | . . | 75 --- | . +-----+ <------+ Scroll Bounds [50,50 - 250,250] (200x200) 76 ^ | . | | . | (Local to Container View, fixed/un-scrolled) 77 | | . | | . | 78 | | . | | . | 79 | | . +-----+ . | 80 | | . . | 81 | +--+---------+---+ 82 | | | 83 -+- | +-----+ | 84 | |#####| | <--+ Requested Bounds [0,300 - 200,400] (200x100) 85 | +-----+ | (Local to Scroll Bounds, fixed/un-scrolled) 86 | | 87 +---------+ 88 89 Container View (ScrollView) [0,0 - 300,300] (scrollY = 200) 90 \__ Content [25,25 - 275,1025] (250x1000) (contentView) 91 \__ Scroll Bounds[50,50 - 250,250] (w=200,h=200) 92 \__ Requested Bounds[0,300 - 200,400] (200x100) 93 */ 94 95 // 0) adjust the requestRect to account for scroll change since start 96 // 97 // Scroll Bounds[50,50 - 250,250] (w=200,h=200) 98 // \__ Requested Bounds[0,200 - 200,300] (200x100) 99 100 // (y-100) (scrollY - mStartScrollY) 101 int scrollDelta = view.getScrollY() - mStartScrollY; 102 103 final ScrollResult result = new ScrollResult(); 104 result.requestedArea = new Rect(requestRect); 105 result.scrollDelta = scrollDelta; 106 result.availableArea = new Rect(); 107 108 final View contentView = view.getChildAt(0); // returns null, does not throw IOOBE 109 if (contentView == null) { 110 // No child view? Cannot continue. 111 resultConsumer.accept(result); 112 return; 113 } 114 115 // 1) Translate request rect to make it relative to container view 116 // 117 // Container View [0,0 - 300,300] (scrollY=200) 118 // \__ Requested Bounds[50,250 - 250,350] (w=250, h=100) 119 120 // (x+50,y+50) 121 Rect requestedContainerBounds = new Rect(requestRect); 122 requestedContainerBounds.offset(0, -scrollDelta); 123 requestedContainerBounds.offset(scrollBounds.left, scrollBounds.top); 124 125 // 2) Translate from container to contentView relative (applying container scrollY) 126 // 127 // Container View [0,0 - 300,300] (scrollY=200) 128 // \__ Content [25,25 - 275,1025] (250x1000) (contentView) 129 // \__ Requested Bounds[25,425 - 200,525] (w=250, h=100) 130 131 // (x-25,y+175) (scrollY - content.top) 132 Rect requestedContentBounds = new Rect(requestedContainerBounds); 133 requestedContentBounds.offset( 134 view.getScrollX() - contentView.getLeft(), 135 view.getScrollY() - contentView.getTop()); 136 137 Rect input = new Rect(requestedContentBounds); 138 139 // Expand input rect to get the requested rect to be in the center 140 int remainingHeight = view.getHeight() - view.getPaddingTop() 141 - view.getPaddingBottom() - input.height(); 142 if (remainingHeight > 0) { 143 input.inset(0, -remainingHeight / 2); 144 } 145 146 // requestRect is now local to contentView as requestedContentBounds 147 // contentView (and each parent in turn if possible) will be scrolled 148 // (if necessary) to make all of requestedContent visible, (if possible!) 149 contentView.requestRectangleOnScreen(input, true); 150 151 // update new offset between starting and current scroll position 152 scrollDelta = view.getScrollY() - mStartScrollY; 153 result.scrollDelta = scrollDelta; 154 155 // TODO: crop capture area to avoid occlusions/minimize scroll changes 156 157 Point offset = new Point(); 158 final Rect available = new Rect(requestedContentBounds); 159 if (!view.getChildVisibleRect(contentView, available, offset)) { 160 available.setEmpty(); 161 result.availableArea = available; 162 resultConsumer.accept(result); 163 return; 164 } 165 // Transform back from global to content-view local 166 available.offset(-offset.x, -offset.y); 167 168 // Then back to container view 169 available.offset( 170 contentView.getLeft() - view.getScrollX(), 171 contentView.getTop() - view.getScrollY()); 172 173 174 // And back to relative to scrollBounds 175 available.offset(-scrollBounds.left, -scrollBounds.top); 176 177 // Apply scrollDelta again to return to make `available` relative to `scrollBounds` at 178 // the scroll position at start of capture. 179 available.offset(0, scrollDelta); 180 181 result.availableArea = new Rect(available); 182 resultConsumer.accept(result); 183 } 184 onPrepareForEnd(@onNull ViewGroup view)185 public void onPrepareForEnd(@NonNull ViewGroup view) { 186 view.scrollTo(0, mStartScrollY); 187 if (mOverScrollMode != View.OVER_SCROLL_NEVER) { 188 view.setOverScrollMode(mOverScrollMode); 189 } 190 if (mScrollBarEnabled) { 191 view.setVerticalScrollBarEnabled(true); 192 } 193 } 194 } 195