1 /*
2  * Copyright (C) 2023 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 package com.android.internal.widget.remotecompose.player.platform;
17 
18 import android.content.Context;
19 import android.graphics.Canvas;
20 import android.graphics.Color;
21 import android.graphics.Point;
22 import android.util.AttributeSet;
23 import android.view.MotionEvent;
24 import android.view.View;
25 import android.widget.FrameLayout;
26 
27 import com.android.internal.widget.remotecompose.core.CoreDocument;
28 import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior;
29 import com.android.internal.widget.remotecompose.core.operations.Theme;
30 import com.android.internal.widget.remotecompose.player.RemoteComposeDocument;
31 
32 import java.util.Set;
33 
34 /**
35  * Internal view handling the actual painting / interactions
36  */
37 public class RemoteComposeCanvas extends FrameLayout implements View.OnAttachStateChangeListener {
38 
39     static final boolean USE_VIEW_AREA_CLICK = true; // Use views to represent click areas
40     RemoteComposeDocument mDocument = null;
41     int mTheme = Theme.LIGHT;
42     boolean mInActionDown = false;
43     boolean mDebug = false;
44     Point mActionDownPoint = new Point(0, 0);
45 
RemoteComposeCanvas(Context context)46     public RemoteComposeCanvas(Context context) {
47         super(context);
48         if (USE_VIEW_AREA_CLICK) {
49             addOnAttachStateChangeListener(this);
50         }
51     }
52 
RemoteComposeCanvas(Context context, AttributeSet attrs)53     public RemoteComposeCanvas(Context context, AttributeSet attrs) {
54         super(context, attrs);
55         if (USE_VIEW_AREA_CLICK) {
56             addOnAttachStateChangeListener(this);
57         }
58     }
59 
RemoteComposeCanvas(Context context, AttributeSet attrs, int defStyleAttr)60     public RemoteComposeCanvas(Context context, AttributeSet attrs, int defStyleAttr) {
61         super(context, attrs, defStyleAttr);
62         setBackgroundColor(Color.WHITE);
63         if (USE_VIEW_AREA_CLICK) {
64             addOnAttachStateChangeListener(this);
65         }
66     }
67 
setDebug(boolean value)68     public void setDebug(boolean value) {
69         if (mDebug != value) {
70             mDebug = value;
71             if (USE_VIEW_AREA_CLICK) {
72                 for (int i = 0; i < getChildCount(); i++) {
73                     View child = getChildAt(i);
74                     if (child instanceof ClickAreaView) {
75                         ((ClickAreaView) child).setDebug(mDebug);
76                     }
77                 }
78             }
79             invalidate();
80         }
81     }
82 
setDocument(RemoteComposeDocument value)83     public void setDocument(RemoteComposeDocument value) {
84         mDocument = value;
85         mDocument.initializeContext(mARContext);
86         setContentDescription(mDocument.getDocument().getContentDescription());
87         requestLayout();
88         invalidate();
89     }
90 
91     AndroidRemoteContext mARContext = new AndroidRemoteContext();
92 
93     @Override
onViewAttachedToWindow(View view)94     public void onViewAttachedToWindow(View view) {
95         if (mDocument == null) {
96             return;
97         }
98         Set<CoreDocument.ClickAreaRepresentation> clickAreas = mDocument
99                 .getDocument().getClickAreas();
100         removeAllViews();
101         for (CoreDocument.ClickAreaRepresentation area : clickAreas) {
102             ClickAreaView viewArea = new ClickAreaView(getContext(), mDebug,
103                     area.getId(), area.getContentDescription(),
104                     area.getMetadata());
105             int w = (int) area.width();
106             int h = (int) area.height();
107             FrameLayout.LayoutParams param = new FrameLayout.LayoutParams(w, h);
108             param.width = w;
109             param.height = h;
110             param.leftMargin = (int) area.getLeft();
111             param.topMargin = (int) area.getTop();
112             viewArea.setOnClickListener(view1
113                     -> mDocument.getDocument().performClick(area.getId()));
114             addView(viewArea, param);
115         }
116     }
117 
118     @Override
onViewDetachedFromWindow(View view)119     public void onViewDetachedFromWindow(View view) {
120         removeAllViews();
121     }
122 
123     public interface ClickCallbacks {
click(int id, String metadata)124         void click(int id, String metadata);
125     }
126 
addClickListener(ClickCallbacks callback)127     public void addClickListener(ClickCallbacks callback) {
128         if (mDocument == null) {
129             return;
130         }
131         mDocument.getDocument().addClickListener((id, metadata) -> callback.click(id, metadata));
132     }
133 
getTheme()134     public int getTheme() {
135         return mTheme;
136     }
137 
setTheme(int theme)138     public void setTheme(int theme) {
139         this.mTheme = theme;
140     }
141 
onTouchEvent(MotionEvent event)142     public boolean onTouchEvent(MotionEvent event) {
143         if (USE_VIEW_AREA_CLICK) {
144             return super.onTouchEvent(event);
145         }
146         switch (event.getActionMasked()) {
147             case MotionEvent.ACTION_DOWN: {
148                 mActionDownPoint.x = (int) event.getX();
149                 mActionDownPoint.y = (int) event.getY();
150                 mInActionDown = true;
151                 return true;
152             }
153             case MotionEvent.ACTION_CANCEL: {
154                 mInActionDown = false;
155                 return true;
156             }
157             case MotionEvent.ACTION_UP: {
158                 mInActionDown = false;
159                 performClick();
160                 return true;
161             }
162             case MotionEvent.ACTION_MOVE: {
163             }
164         }
165         return false;
166     }
167 
168     @Override
performClick()169     public boolean performClick() {
170         if (USE_VIEW_AREA_CLICK) {
171             return super.performClick();
172         }
173         mDocument.getDocument().onClick((float) mActionDownPoint.x, (float) mActionDownPoint.y);
174         super.performClick();
175         return true;
176     }
177 
measureDimension(int measureSpec, int intrinsicSize)178     public int measureDimension(int measureSpec, int intrinsicSize) {
179         int result = intrinsicSize;
180         int mode = MeasureSpec.getMode(measureSpec);
181         int size = MeasureSpec.getSize(measureSpec);
182         switch (mode) {
183             case MeasureSpec.EXACTLY:
184                 result = size;
185                 break;
186             case MeasureSpec.AT_MOST:
187                 result = Integer.min(size, intrinsicSize);
188                 break;
189             case MeasureSpec.UNSPECIFIED:
190                 result = intrinsicSize;
191         }
192         return result;
193     }
194 
195     private static final float[] sScaleOutput = new float[2];
196 
197     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)198     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
199         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
200         if (mDocument == null) {
201             return;
202         }
203         int w = measureDimension(widthMeasureSpec, mDocument.getWidth());
204         int h = measureDimension(heightMeasureSpec, mDocument.getHeight());
205 
206         if (!USE_VIEW_AREA_CLICK) {
207             if (mDocument.getDocument().getContentSizing() == RootContentBehavior.SIZING_SCALE) {
208                 mDocument.getDocument().computeScale(w, h, sScaleOutput);
209                 w = (int) (mDocument.getWidth() * sScaleOutput[0]);
210                 h = (int) (mDocument.getHeight() * sScaleOutput[1]);
211             }
212         }
213         setMeasuredDimension(w, h);
214     }
215 
216     private int mCount;
217     private long mTime = System.nanoTime();
218 
219     @Override
onDraw(Canvas canvas)220     protected void onDraw(Canvas canvas) {
221         super.onDraw(canvas);
222         if (mDocument == null) {
223             return;
224         }
225         mARContext.setDebug(mDebug);
226         mARContext.useCanvas(canvas);
227         mARContext.mWidth = getWidth();
228         mARContext.mHeight = getHeight();
229         mDocument.paint(mARContext, mTheme);
230         if (mDebug) {
231             mCount++;
232             if (System.nanoTime() - mTime > 1000000000L) {
233                 System.out.println(" count " + mCount + " fps");
234                 mCount = 0;
235                 mTime = System.nanoTime();
236             }
237         }
238         if (mDocument.needsRepaint() > 0) {
239             invalidate();
240         }
241     }
242 
243 }
244 
245