InlineSuggestionsHostView.java
/*
* Copyright 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 androidx.autofill;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.SurfaceControl;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.AttrRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.collection.ArraySet;
/**
* This class is a container for showing inline suggestions for cases where you
* want to ensure they appear only in a given area in your app. An example is
* having a scrollable list of suggestions with an icon on the left and one on
* the right (think of a suggestion strip with actions on left, right and a
* scrollable suggestions list in the middle) where scrolling the suggestions
* should not cover the icons on both sides. Note that without this container
* the suggestions would cover parts of your app as they are surfaces owned
* by another process and always appearing on top of your app.
*/
@RequiresApi(api = Build.VERSION_CODES.Q) // TODO(b/147116534): Update to R
public class InlineSuggestionsHostView extends FrameLayout {
// The trick that we use here is to have a hidden SurfaceView to whose
// surface we reparent the surfaces of remote suggestions which are
// also SurfaceViews. Since surface locations are based off the window
// top-left making, making one surface parent of another compounds the
// offset from the child's point of view. To compensate for that we
// add a FrameLayout wrapping each child and set the FrameLayout's scroll
// to be that of the parent surface - compensating the compounding.
private final @NonNull ArraySet<SurfaceView> mReparentedChildren = new ArraySet<>();
private final @NonNull int[] mTempLocation = new int[2];
private final @NonNull SurfaceView mSurfaceClipView;
public InlineSuggestionsHostView(@NonNull Context context) {
this(context, /*attrs*/ null);
}
public InlineSuggestionsHostView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, /*defStyleAttr*/0);
}
public InlineSuggestionsHostView(@NonNull Context context, @Nullable AttributeSet attrs,
@AttrRes int defStyleAttr) {
super(context, attrs, defStyleAttr);
mSurfaceClipView = new SurfaceView(context);
mSurfaceClipView.setZOrderOnTop(true);
mSurfaceClipView.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
mSurfaceClipView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
/* do nothing */
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width,
int height) {
/* do nothing */
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
updateState(InlineSuggestionsHostView.this,
/*parentSurfaceProvider*/ null);
}
});
super.addView(mSurfaceClipView);
// This is needed to handle the surfaces of the suggestions being created later and
// also to keep things in sync. The update method is optimized to be called often.
getViewTreeObserver().addOnGlobalLayoutListener(
() -> updateState(this, mSurfaceClipView));
}
@Override
public void addView(@NonNull View child, int index, @NonNull ViewGroup.LayoutParams params) {
if (child == mSurfaceClipView) {
super.addView(child, index, params);
return;
}
final FrameLayout locationOffsetWrapper = new FrameLayout(getContext());
locationOffsetWrapper.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
locationOffsetWrapper.addView(child);
super.addView(locationOffsetWrapper, index, params);
updateState(InlineSuggestionsHostView.this, mSurfaceClipView);
}
@Override
public void removeView(@Nullable View view) {
if (view == null) {
return;
}
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (child == view) {
super.removeView(child);
updateState(view, /*parentSurfaceProvider*/ null);
return;
}
if (!(child instanceof ViewGroup)) {
continue;
}
final ViewGroup childGroup = (ViewGroup) child;
final int grandChildCount = childGroup.getChildCount();
for (int j = 0; j < grandChildCount; j++) {
final View grandChild = childGroup.getChildAt(j);
if (grandChild == view) {
super.removeView(child);
updateState(view, /*parentSurfaceProvider*/ null);
return;
}
}
}
}
void updateState(@NonNull View root,
@Nullable SurfaceView parentSurfaceProvider) {
mSurfaceClipView.getLocationInWindow(mTempLocation);
reparentChildSurfaceViewSurfacesRecursive(root, parentSurfaceProvider,
/*parentSurfaceLeft*/ mTempLocation[0],
/*parentSurfaceTop*/ mTempLocation[1]);
}
private void reparentChildSurfaceViewSurfacesRecursive(@Nullable View root,
@Nullable SurfaceView parentSurfaceProvider, int parentSurfaceLeft,
int parentSurfaceTop) {
if (mSurfaceClipView.getSurfaceControl() == null
|| !mSurfaceClipView.getSurfaceControl().isValid()) {
return;
}
if (!(root instanceof ViewGroup)) {
return;
}
final ViewGroup rootGroup = (ViewGroup) root;
final int childCount = rootGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = rootGroup.getChildAt(i);
if (child == mSurfaceClipView) {
continue;
}
if (child instanceof SurfaceView) {
final SurfaceView childSurfaceView = (SurfaceView) child;
if (childSurfaceView.getSurfaceControl() == null
|| !childSurfaceView.getSurfaceControl().isValid()) {
continue;
}
if (parentSurfaceProvider != null) {
if (mReparentedChildren.contains(childSurfaceView)) {
continue;
}
new SurfaceControl.Transaction()
.reparent(childSurfaceView.getSurfaceControl(),
parentSurfaceProvider.getSurfaceControl())
.apply();
root.setScrollX(parentSurfaceLeft);
root.setScrollY(parentSurfaceTop);
mReparentedChildren.add(childSurfaceView);
} else {
if (!mReparentedChildren.contains(childSurfaceView)) {
continue;
}
new SurfaceControl.Transaction()
.reparent(childSurfaceView.getSurfaceControl(),
/*newParent*/null)
.apply();
root.setScrollX(0);
root.setScrollY(0);
mReparentedChildren.remove(childSurfaceView);
}
}
reparentChildSurfaceViewSurfacesRecursive(child, parentSurfaceProvider,
parentSurfaceLeft, parentSurfaceTop);
}
}
}