195 lines
7.4 KiB
Java
195 lines
7.4 KiB
Java
/*
|
|
* Copyright (C) 2020 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.android.internal.view;
|
|
|
|
import android.annotation.NonNull;
|
|
import android.graphics.Point;
|
|
import android.graphics.Rect;
|
|
import android.os.CancellationSignal;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.view.ViewParent;
|
|
|
|
import java.util.function.Consumer;
|
|
|
|
/**
|
|
* ScrollCapture for ScrollView and <i>ScrollView-like</i> ViewGroups.
|
|
* <p>
|
|
* Requirements for proper operation:
|
|
* <ul>
|
|
* <li>contains at most 1 child.
|
|
* <li>scrolls to absolute positions with {@link View#scrollTo(int, int)}.
|
|
* <li>has a finite, known content height and scrolling range
|
|
* <li>correctly implements {@link View#canScrollVertically(int)}
|
|
* <li>correctly implements {@link ViewParent#requestChildRectangleOnScreen(View,
|
|
* Rect, boolean)}
|
|
* </ul>
|
|
*
|
|
* @see ScrollCaptureViewSupport
|
|
*/
|
|
public class ScrollViewCaptureHelper implements ScrollCaptureViewHelper<ViewGroup> {
|
|
private int mStartScrollY;
|
|
private boolean mScrollBarEnabled;
|
|
private int mOverScrollMode;
|
|
|
|
public boolean onAcceptSession(@NonNull ViewGroup view) {
|
|
return view.isVisibleToUser()
|
|
&& (view.canScrollVertically(UP) || view.canScrollVertically(DOWN));
|
|
}
|
|
|
|
public void onPrepareForStart(@NonNull ViewGroup view, Rect scrollBounds) {
|
|
mStartScrollY = view.getScrollY();
|
|
mOverScrollMode = view.getOverScrollMode();
|
|
if (mOverScrollMode != View.OVER_SCROLL_NEVER) {
|
|
view.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
|
}
|
|
mScrollBarEnabled = view.isVerticalScrollBarEnabled();
|
|
if (mScrollBarEnabled) {
|
|
view.setVerticalScrollBarEnabled(false);
|
|
}
|
|
}
|
|
|
|
public void onScrollRequested(@NonNull ViewGroup view, Rect scrollBounds,
|
|
Rect requestRect, CancellationSignal signal, Consumer<ScrollResult> resultConsumer) {
|
|
/*
|
|
+---------+ <----+ Content [25,25 - 275,1025] (w=250,h=1000)
|
|
| |
|
|
...|.........|... startScrollY=100
|
|
| |
|
|
+--+---------+---+ <--+ Container View [0,0 - 300,300] (scrollY=200)
|
|
| . . |
|
|
--- | . +-----+ <------+ Scroll Bounds [50,50 - 250,250] (200x200)
|
|
^ | . | | . | (Local to Container View, fixed/un-scrolled)
|
|
| | . | | . |
|
|
| | . | | . |
|
|
| | . +-----+ . |
|
|
| | . . |
|
|
| +--+---------+---+
|
|
| | |
|
|
-+- | +-----+ |
|
|
| |#####| | <--+ Requested Bounds [0,300 - 200,400] (200x100)
|
|
| +-----+ | (Local to Scroll Bounds, fixed/un-scrolled)
|
|
| |
|
|
+---------+
|
|
|
|
Container View (ScrollView) [0,0 - 300,300] (scrollY = 200)
|
|
\__ Content [25,25 - 275,1025] (250x1000) (contentView)
|
|
\__ Scroll Bounds[50,50 - 250,250] (w=200,h=200)
|
|
\__ Requested Bounds[0,300 - 200,400] (200x100)
|
|
*/
|
|
|
|
// 0) adjust the requestRect to account for scroll change since start
|
|
//
|
|
// Scroll Bounds[50,50 - 250,250] (w=200,h=200)
|
|
// \__ Requested Bounds[0,200 - 200,300] (200x100)
|
|
|
|
// (y-100) (scrollY - mStartScrollY)
|
|
int scrollDelta = view.getScrollY() - mStartScrollY;
|
|
|
|
final ScrollResult result = new ScrollResult();
|
|
result.requestedArea = new Rect(requestRect);
|
|
result.scrollDelta = scrollDelta;
|
|
result.availableArea = new Rect();
|
|
|
|
final View contentView = view.getChildAt(0); // returns null, does not throw IOOBE
|
|
if (contentView == null) {
|
|
// No child view? Cannot continue.
|
|
resultConsumer.accept(result);
|
|
return;
|
|
}
|
|
|
|
// 1) Translate request rect to make it relative to container view
|
|
//
|
|
// Container View [0,0 - 300,300] (scrollY=200)
|
|
// \__ Requested Bounds[50,250 - 250,350] (w=250, h=100)
|
|
|
|
// (x+50,y+50)
|
|
Rect requestedContainerBounds = new Rect(requestRect);
|
|
requestedContainerBounds.offset(0, -scrollDelta);
|
|
requestedContainerBounds.offset(scrollBounds.left, scrollBounds.top);
|
|
|
|
// 2) Translate from container to contentView relative (applying container scrollY)
|
|
//
|
|
// Container View [0,0 - 300,300] (scrollY=200)
|
|
// \__ Content [25,25 - 275,1025] (250x1000) (contentView)
|
|
// \__ Requested Bounds[25,425 - 200,525] (w=250, h=100)
|
|
|
|
// (x-25,y+175) (scrollY - content.top)
|
|
Rect requestedContentBounds = new Rect(requestedContainerBounds);
|
|
requestedContentBounds.offset(
|
|
view.getScrollX() - contentView.getLeft(),
|
|
view.getScrollY() - contentView.getTop());
|
|
|
|
Rect input = new Rect(requestedContentBounds);
|
|
|
|
// Expand input rect to get the requested rect to be in the center
|
|
int remainingHeight = view.getHeight() - view.getPaddingTop()
|
|
- view.getPaddingBottom() - input.height();
|
|
if (remainingHeight > 0) {
|
|
input.inset(0, -remainingHeight / 2);
|
|
}
|
|
|
|
// requestRect is now local to contentView as requestedContentBounds
|
|
// contentView (and each parent in turn if possible) will be scrolled
|
|
// (if necessary) to make all of requestedContent visible, (if possible!)
|
|
contentView.requestRectangleOnScreen(input, true);
|
|
|
|
// update new offset between starting and current scroll position
|
|
scrollDelta = view.getScrollY() - mStartScrollY;
|
|
result.scrollDelta = scrollDelta;
|
|
|
|
// TODO: crop capture area to avoid occlusions/minimize scroll changes
|
|
|
|
Point offset = new Point();
|
|
final Rect available = new Rect(requestedContentBounds);
|
|
if (!view.getChildVisibleRect(contentView, available, offset)) {
|
|
available.setEmpty();
|
|
result.availableArea = available;
|
|
resultConsumer.accept(result);
|
|
return;
|
|
}
|
|
// Transform back from global to content-view local
|
|
available.offset(-offset.x, -offset.y);
|
|
|
|
// Then back to container view
|
|
available.offset(
|
|
contentView.getLeft() - view.getScrollX(),
|
|
contentView.getTop() - view.getScrollY());
|
|
|
|
|
|
// And back to relative to scrollBounds
|
|
available.offset(-scrollBounds.left, -scrollBounds.top);
|
|
|
|
// Apply scrollDelta again to return to make `available` relative to `scrollBounds` at
|
|
// the scroll position at start of capture.
|
|
available.offset(0, scrollDelta);
|
|
|
|
result.availableArea = new Rect(available);
|
|
resultConsumer.accept(result);
|
|
}
|
|
|
|
public void onPrepareForEnd(@NonNull ViewGroup view) {
|
|
view.scrollTo(0, mStartScrollY);
|
|
if (mOverScrollMode != View.OVER_SCROLL_NEVER) {
|
|
view.setOverScrollMode(mOverScrollMode);
|
|
}
|
|
if (mScrollBarEnabled) {
|
|
view.setVerticalScrollBarEnabled(true);
|
|
}
|
|
}
|
|
}
|