FreeRDP
Loading...
Searching...
No Matches
FloatingToolbar.java
1/*
2 Floating toolbar for RDP session controls
3
4 Copyright 2026 Ibrahim Sevinc <ibrahim.sevinc.mail@gmail.com>
5
6 This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
7 If a copy of the MPL was not distributed with this file, You can obtain one at
8 http://mozilla.org/MPL/2.0/.
9*/
10
11package com.freerdp.freerdpcore.presentation;
12
13import android.app.Activity;
14import android.view.GestureDetector;
15import android.view.MotionEvent;
16import android.view.View;
17import android.view.ViewTreeObserver;
18import android.widget.LinearLayout;
19
20import com.freerdp.freerdpcore.R;
21
22public class FloatingToolbar
23{
24 public interface Listener
25 {
26 void onToggleTouchPointer();
27 void onToggleSysKeyboard();
28 void onToggleExtKeyboard();
29 }
30
31 private enum Edge
32 {
33 LEFT,
34 RIGHT,
35 TOP,
36 BOTTOM
37 }
38
39 private final LinearLayout container;
40 private final LinearLayout buttons;
41 private boolean expanded = false;
42 private int insetLeft = 0, insetTop = 0, insetRight = 0, insetBottom = 0;
43 private Edge snappedEdge = Edge.LEFT;
44 private float snappedFraction = 0.4f;
45
46 public void setInsets(int left, int top, int right, int bottom)
47 {
48 insetLeft = left;
49 insetTop = top;
50 insetRight = right;
51 insetBottom = bottom;
52 }
53
54 public FloatingToolbar(Activity activity, Listener listener)
55 {
56 container = activity.findViewById(R.id.floating_toolbar_container);
57 buttons = activity.findViewById(R.id.floating_toolbar_buttons);
58 View handle = activity.findViewById(R.id.floating_toolbar_handle);
59 setTooltip(handle);
60
61 GestureDetector gestureDetector =
62 new GestureDetector(activity, new GestureDetector.SimpleOnGestureListener() {
63 @Override public boolean onSingleTapConfirmed(MotionEvent e)
64 {
65 toggle();
66 return true;
67 }
68 });
69
70 View.OnTouchListener dragListener = buildDragListener(activity, gestureDetector, handle);
71 container.setOnTouchListener(dragListener);
72 handle.setOnTouchListener(dragListener);
73 for (int i = 0; i < buttons.getChildCount(); i++)
74 buttons.getChildAt(i).setOnTouchListener(dragListener);
75
76 bindButton(activity, R.id.floating_toolbar_touch_pointer, listener::onToggleTouchPointer);
77 bindButton(activity, R.id.floating_toolbar_sys_keyboard, listener::onToggleSysKeyboard);
78 bindButton(activity, R.id.floating_toolbar_ext_keyboard, listener::onToggleExtKeyboard);
79
80 ViewTreeObserver vto = container.getViewTreeObserver();
81 vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
82 @Override public void onGlobalLayout()
83 {
84 container.getViewTreeObserver().removeOnGlobalLayoutListener(this);
85 View parent = (View)container.getParent();
86 if (parent == null)
87 return;
88 positionInitial(parent);
89 parent.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop,
90 oldRight, oldBottom) -> {
91 if (right - left != oldRight - oldLeft || bottom - top != oldBottom - oldTop)
92 resnapToSameEdge();
93 });
94 }
95 });
96 }
97
98 private void positionInitial(View parent)
99 {
100 container.setOrientation(LinearLayout.VERTICAL);
101 buttons.setOrientation(LinearLayout.VERTICAL);
102 snappedEdge = Edge.LEFT;
103 snappedFraction = 0.4f;
104 float ph = parent.getHeight();
105 float ch = container.getHeight();
106 container.setX(insetLeft);
107 container.setY(
108 Math.max(insetTop, insetTop + snappedFraction * (ph - insetTop - insetBottom - ch)));
109 }
110
111 private void resnapToSameEdge()
112 {
113 View parent = (View)container.getParent();
114 if (parent == null)
115 return;
116
117 int orientation = (snappedEdge == Edge.LEFT || snappedEdge == Edge.RIGHT)
118 ? LinearLayout.VERTICAL
119 : LinearLayout.HORIZONTAL;
120 container.setOrientation(orientation);
121 buttons.setOrientation(orientation);
122
123 container.post(() -> {
124 float pw = parent.getWidth(), ph = parent.getHeight();
125 float w = container.getWidth(), h = container.getHeight();
126 float tx, ty;
127 switch (snappedEdge)
128 {
129 case LEFT:
130 tx = insetLeft;
131 ty = insetTop + snappedFraction * (ph - insetTop - insetBottom - h);
132 break;
133 case RIGHT:
134 tx = pw - w - insetRight;
135 ty = insetTop + snappedFraction * (ph - insetTop - insetBottom - h);
136 break;
137 case TOP:
138 tx = insetLeft + snappedFraction * (pw - insetLeft - insetRight - w);
139 ty = insetTop;
140 break;
141 default:
142 tx = insetLeft + snappedFraction * (pw - insetLeft - insetRight - w);
143 ty = ph - h - insetBottom;
144 break;
145 }
146 tx = Math.max(insetLeft, Math.min(tx, pw - w - insetRight));
147 ty = Math.max(insetTop, Math.min(ty, ph - h - insetBottom));
148 container.animate().x(tx).y(ty).setDuration(250).start();
149 });
150 }
151
152 private void toggle()
153 {
154 expanded = !expanded;
155 buttons.setVisibility(expanded ? View.VISIBLE : View.GONE);
156 snap();
157 }
158
159 private void bindButton(Activity activity, int id, Runnable action)
160 {
161 View v = activity.findViewById(id);
162 if (v != null)
163 {
164 setTooltip(v);
165 v.setOnClickListener(ignored -> action.run());
166 }
167 }
168
169 private static void setTooltip(View v)
170 {
171 v.setTooltipText(v.getContentDescription());
172 }
173
174 private View.OnTouchListener buildDragListener(Activity activity,
175 GestureDetector gestureDetector, View handle)
176 {
177 int touchSlop = android.view.ViewConfiguration.get(activity).getScaledTouchSlop();
178 return new View.OnTouchListener() {
179 private float startX, startY, offsetX, offsetY;
180 private boolean dragging;
181
182 @Override public boolean onTouch(View v, MotionEvent e)
183 {
184 if (v == handle || v == container)
185 gestureDetector.onTouchEvent(e);
186
187 switch (e.getActionMasked())
188 {
189 case MotionEvent.ACTION_DOWN:
190 startX = e.getRawX();
191 startY = e.getRawY();
192 offsetX = container.getX() - startX;
193 offsetY = container.getY() - startY;
194 dragging = false;
195 v.onTouchEvent(e);
196 break;
197 case MotionEvent.ACTION_MOVE:
198 if (!dragging && (Math.abs(e.getRawX() - startX) > touchSlop ||
199 Math.abs(e.getRawY() - startY) > touchSlop))
200 {
201 dragging = true;
202 MotionEvent cancel = MotionEvent.obtain(e);
203 cancel.setAction(MotionEvent.ACTION_CANCEL);
204 v.onTouchEvent(cancel);
205 cancel.recycle();
206 }
207 if (dragging)
208 container.animate()
209 .x(e.getRawX() + offsetX)
210 .y(e.getRawY() + offsetY)
211 .setDuration(0)
212 .start();
213 else
214 v.onTouchEvent(e);
215 break;
216 case MotionEvent.ACTION_UP:
217 case MotionEvent.ACTION_CANCEL:
218 if (!dragging)
219 v.onTouchEvent(e);
220 snap();
221 break;
222 }
223 return true;
224 }
225 };
226 }
227
228 private void snap()
229 {
230 View parent = (View)container.getParent();
231 if (parent == null)
232 return;
233
234 float px = container.getX(), py = container.getY();
235 float pw = parent.getWidth(), ph = parent.getHeight();
236 float cw = container.getWidth(), ch = container.getHeight();
237
238 float dLeft = px - insetLeft;
239 float dRight = Math.max(0, pw - px - cw - insetRight);
240 float dTop = py - insetTop;
241 float dBottom = Math.max(0, ph - py - ch - insetBottom);
242 float min = Math.min(Math.min(dLeft, dRight), Math.min(dTop, dBottom));
243
244 if (min == dLeft)
245 snappedEdge = Edge.LEFT;
246 else if (min == dRight)
247 snappedEdge = Edge.RIGHT;
248 else if (min == dTop)
249 snappedEdge = Edge.TOP;
250 else
251 snappedEdge = Edge.BOTTOM;
252
253 int orientation = (snappedEdge == Edge.LEFT || snappedEdge == Edge.RIGHT)
254 ? LinearLayout.VERTICAL
255 : LinearLayout.HORIZONTAL;
256 container.setOrientation(orientation);
257 buttons.setOrientation(orientation);
258
259 container.post(() -> {
260 float w = container.getWidth(), h = container.getHeight();
261 float tx = container.getX(), ty = container.getY();
262 switch (snappedEdge)
263 {
264 case LEFT:
265 tx = insetLeft;
266 break;
267 case RIGHT:
268 tx = pw - w - insetRight;
269 break;
270 case TOP:
271 ty = insetTop;
272 break;
273 case BOTTOM:
274 ty = ph - h - insetBottom;
275 break;
276 }
277 tx = Math.max(insetLeft, Math.min(tx, pw - w - insetRight));
278 ty = Math.max(insetTop, Math.min(ty, ph - h - insetBottom));
279 if (snappedEdge == Edge.LEFT || snappedEdge == Edge.RIGHT)
280 {
281 float available = ph - insetTop - insetBottom - h;
282 snappedFraction =
283 available > 0 ? Math.max(0f, Math.min(1f, (ty - insetTop) / available)) : 0.5f;
284 }
285 else
286 {
287 float available = pw - insetLeft - insetRight - w;
288 snappedFraction =
289 available > 0 ? Math.max(0f, Math.min(1f, (tx - insetLeft) / available)) : 0.5f;
290 }
291 container.animate().x(tx).y(ty).setDuration(250).start();
292 });
293 }
294}