1 /*
2  * Copyright 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.example.android.autofillkeyboard;
18 
19 import android.content.Context;
20 import android.graphics.Canvas;
21 import android.graphics.PixelFormat;
22 import android.graphics.Rect;
23 import android.os.Build;
24 import android.util.AttributeSet;
25 import android.view.Choreographer;
26 import android.view.Surface;
27 import android.view.SurfaceHolder;
28 import android.view.SurfaceView;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.view.ViewTreeObserver;
32 import android.widget.inline.InlineContentView;
33 import android.widget.FrameLayout;
34 
35 import androidx.annotation.AttrRes;
36 import androidx.annotation.NonNull;
37 import androidx.annotation.Nullable;
38 import androidx.annotation.RequiresApi;
39 import androidx.collection.ArraySet;
40 
41 /**
42  * This class is a container for showing {@link InlineContentView}s for cases
43  * where you want to ensure they appear only in a given area in your app. An
44  * example is having a scrollable list of items. Note that without this container
45  * the InlineContentViews' surfaces would cover parts of your app as these surfaces
46  * are owned by another process and always appearing on top of your app.
47  */
48 @RequiresApi(api = Build.VERSION_CODES.R)
49 public class InlineContentClipView extends FrameLayout {
50     @NonNull
51     private final ArraySet<InlineContentView> mClippedDescendants = new ArraySet<>();
52 
53     @NonNull
54     private final ViewTreeObserver.OnDrawListener mOnDrawListener =
55             this::clipDescendantInlineContentViews;
56 
57     @NonNull
58     private final Rect mParentBounds = new Rect();
59 
60     @NonNull
61     private final Rect mContentBounds = new Rect();
62 
63     @NonNull
64     private SurfaceView mBackgroundView;
65 
66     private int mBackgroundColor;
67 
InlineContentClipView(@onNull Context context)68     public InlineContentClipView(@NonNull Context context) {
69         this(context, /*attrs*/ null);
70     }
71 
InlineContentClipView(@onNull Context context, @Nullable AttributeSet attrs)72     public InlineContentClipView(@NonNull Context context, @Nullable AttributeSet attrs) {
73         this(context, attrs, /*defStyleAttr*/ 0);
74     }
75 
InlineContentClipView(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr)76     public InlineContentClipView(@NonNull Context context, @Nullable AttributeSet attrs,
77             @AttrRes int defStyleAttr) {
78         super(context, attrs, defStyleAttr);
79 
80         mBackgroundView = new SurfaceView(context);
81         mBackgroundView.setZOrderOnTop(true);
82         mBackgroundView.getHolder().setFormat(PixelFormat.TRANSPARENT);
83         mBackgroundView.setLayoutParams(new ViewGroup.LayoutParams(
84                 ViewGroup.LayoutParams.WRAP_CONTENT,
85                 ViewGroup.LayoutParams.WRAP_CONTENT));
86         mBackgroundView.getHolder().addCallback(new SurfaceHolder.Callback() {
87             @Override
88             public void surfaceCreated(@NonNull SurfaceHolder holder) {
89                 drawBackgroundColorIfReady();
90             }
91 
92             @Override
93             public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width,
94                     int height) { /*do nothing*/ }
95 
96             @Override
97             public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
98                 /*do nothing*/
99             }
100         });
101 
102         addView(mBackgroundView);
103     }
104 
105     @Override
onAttachedToWindow()106     protected void onAttachedToWindow() {
107         super.onAttachedToWindow();
108         getViewTreeObserver().addOnDrawListener(mOnDrawListener);
109     }
110 
111     @Override
onDetachedFromWindow()112     protected void onDetachedFromWindow() {
113         super.onDetachedFromWindow();
114         getViewTreeObserver().removeOnDrawListener(mOnDrawListener);
115     }
116 
117     @Override
setBackgroundColor(int color)118     public void setBackgroundColor(int color) {
119         mBackgroundColor = color;
120         Choreographer.getInstance().postFrameCallback((frameTimeNanos) ->
121                 drawBackgroundColorIfReady());
122     }
123 
drawBackgroundColorIfReady()124     private void drawBackgroundColorIfReady() {
125         final Surface surface = mBackgroundView.getHolder().getSurface();
126         if (surface.isValid()) {
127             final Canvas canvas = surface.lockCanvas(null);
128             try {
129                 canvas.drawColor(mBackgroundColor);
130             } finally {
131                 surface.unlockCanvasAndPost(canvas);
132             }
133         }
134     }
135 
136     /**
137      * Sets whether the surfaces of the {@link InlineContentView}s wrapped by this view
138      * should appear on top or behind this view's window. Normally, they are placed on top
139      * of the window, to allow interaction ith the embedded UI. Via this method, you can
140      * place the surface below the window. This means that all of the contents of the window
141      * this view is in will be visible on top of the  {@link InlineContentView}s' surfaces.
142      *
143      * @param onTop Whether to show the surface on top of this view's window.
144      *
145      * @see InlineContentView
146      * @see InlineContentView#setZOrderedOnTop(boolean)
147      */
setZOrderedOnTop(boolean onTop)148     public void setZOrderedOnTop(boolean onTop) {
149         mBackgroundView.setZOrderOnTop(onTop);
150         for (InlineContentView inlineContentView : mClippedDescendants) {
151             inlineContentView.setZOrderedOnTop(onTop);
152         }
153     }
154 
clipDescendantInlineContentViews()155     private void clipDescendantInlineContentViews() {
156         mParentBounds.right = getWidth();
157         mParentBounds.bottom = getHeight();
158         mClippedDescendants.clear();
159         clipDescendantInlineContentViews(this);
160     }
161 
clipDescendantInlineContentViews(@ullable View root)162     private void clipDescendantInlineContentViews(@Nullable View root) {
163         if (root == null) {
164             return;
165         }
166 
167         if (root instanceof InlineContentView) {
168             final InlineContentView inlineContentView = (InlineContentView) root;
169             mContentBounds.set(mParentBounds);
170             offsetRectIntoDescendantCoords(inlineContentView, mContentBounds);
171             inlineContentView.setClipBounds(mContentBounds);
172             mClippedDescendants.add(inlineContentView);
173             return;
174         }
175 
176         if (root instanceof ViewGroup) {
177             final ViewGroup rootGroup = (ViewGroup) root;
178             final int childCount = rootGroup.getChildCount();
179             for (int i = 0; i < childCount; i++) {
180                 final View child = rootGroup.getChildAt(i);
181                 clipDescendantInlineContentViews(child);
182             }
183         }
184     }
185 }