/* * 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 ScrollView-like ViewGroups. *

* Requirements for proper operation: *

* * @see ScrollCaptureViewSupport */ public class ScrollViewCaptureHelper implements ScrollCaptureViewHelper { 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 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); } } }