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