blob: b4d2c546aafd622c3dd40706199c5c997dabe99f [file] [log] [blame]
/*
* 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 com.example.android.autofillkeyboard;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.os.Build;
import android.util.AttributeSet;
import android.view.Choreographer;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.inline.InlineContentView;
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 {@link InlineContentView}s 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 items. Note that without this container
* the InlineContentViews' surfaces would cover parts of your app as these surfaces
* are owned by another process and always appearing on top of your app.
*/
@RequiresApi(api = Build.VERSION_CODES.R)
public class InlineContentClipView extends FrameLayout {
@NonNull
private final ArraySet<InlineContentView> mClippedDescendants = new ArraySet<>();
@NonNull
private final ViewTreeObserver.OnDrawListener mOnDrawListener =
this::clipDescendantInlineContentViews;
@NonNull
private final Rect mParentBounds = new Rect();
@NonNull
private final Rect mContentBounds = new Rect();
@NonNull
private SurfaceView mBackgroundView;
private int mBackgroundColor;
public InlineContentClipView(@NonNull Context context) {
this(context, /*attrs*/ null);
}
public InlineContentClipView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, /*defStyleAttr*/ 0);
}
public InlineContentClipView(@NonNull Context context, @Nullable AttributeSet attrs,
@AttrRes int defStyleAttr) {
super(context, attrs, defStyleAttr);
mBackgroundView = new SurfaceView(context);
mBackgroundView.setZOrderOnTop(true);
mBackgroundView.getHolder().setFormat(PixelFormat.TRANSPARENT);
mBackgroundView.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
mBackgroundView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
drawBackgroundColorIfReady();
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width,
int height) { /*do nothing*/ }
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
/*do nothing*/
}
});
addView(mBackgroundView);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
getViewTreeObserver().addOnDrawListener(mOnDrawListener);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
getViewTreeObserver().removeOnDrawListener(mOnDrawListener);
}
@Override
public void setBackgroundColor(int color) {
mBackgroundColor = color;
Choreographer.getInstance().postFrameCallback((frameTimeNanos) ->
drawBackgroundColorIfReady());
}
private void drawBackgroundColorIfReady() {
final Surface surface = mBackgroundView.getHolder().getSurface();
if (surface.isValid()) {
final Canvas canvas = surface.lockCanvas(null);
try {
canvas.drawColor(mBackgroundColor);
} finally {
surface.unlockCanvasAndPost(canvas);
}
}
}
/**
* Sets whether the surfaces of the {@link InlineContentView}s wrapped by this view
* should appear on top or behind this view's window. Normally, they are placed on top
* of the window, to allow interaction ith the embedded UI. Via this method, you can
* place the surface below the window. This means that all of the contents of the window
* this view is in will be visible on top of the {@link InlineContentView}s' surfaces.
*
* @param onTop Whether to show the surface on top of this view's window.
*
* @see InlineContentView
* @see InlineContentView#setZOrderedOnTop(boolean)
*/
public void setZOrderedOnTop(boolean onTop) {
mBackgroundView.setZOrderOnTop(onTop);
for (InlineContentView inlineContentView : mClippedDescendants) {
inlineContentView.setZOrderedOnTop(onTop);
}
}
private void clipDescendantInlineContentViews() {
mParentBounds.right = getWidth();
mParentBounds.bottom = getHeight();
mClippedDescendants.clear();
clipDescendantInlineContentViews(this);
}
private void clipDescendantInlineContentViews(@Nullable View root) {
if (root == null) {
return;
}
if (root instanceof InlineContentView) {
final InlineContentView inlineContentView = (InlineContentView) root;
mContentBounds.set(mParentBounds);
offsetRectIntoDescendantCoords(inlineContentView, mContentBounds);
inlineContentView.setClipBounds(mContentBounds);
mClippedDescendants.add(inlineContentView);
return;
}
if (root instanceof ViewGroup) {
final ViewGroup rootGroup = (ViewGroup) root;
final int childCount = rootGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = rootGroup.getChildAt(i);
clipDescendantInlineContentViews(child);
}
}
}
}