/* * 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 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); } } } }