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