这是indexloc提供的服务,不要输入任何密码
Skip to content

Commit 17a76b2

Browse files
authored
Implement floating "chat head" bubble (#35)
* Implement floating bubble for TermuxFloatView * Add Window Controls toolbar w/ icons * Don't show the long press toast if moving while minimized * Remove focus when displaying as bubble * Focus window if top control bar is tapped
1 parent 0f4cbe3 commit 17a76b2

File tree

7 files changed

+266
-2
lines changed

7 files changed

+266
-2
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package com.termux.window;
2+
3+
import android.graphics.drawable.Drawable;
4+
import android.view.View;
5+
import android.view.ViewGroup;
6+
import android.view.WindowManager;
7+
8+
import com.termux.view.TerminalView;
9+
10+
/**
11+
* Handles displaying our TermuxFloatView as a collapsed bubble and restoring back
12+
* to its original display.
13+
*/
14+
public class FloatingBubbleManager {
15+
private static final int BUBBLE_SIZE = 200;
16+
17+
private TermuxFloatView mTermuxFloatView;
18+
19+
private boolean mIsMinimized;
20+
21+
// preserve original layout values so we can restore to normal window
22+
// from our bubble
23+
private int mOriginalLayoutWidth;
24+
private int mOriginalLayoutHeight;
25+
private boolean mDidCaptureOriginalValues;
26+
private Drawable mOriginalTerminalViewBackground;
27+
private Drawable mOriginalFloatViewBackground;
28+
29+
30+
public FloatingBubbleManager(TermuxFloatView termuxFloatView) {
31+
mTermuxFloatView = termuxFloatView;
32+
}
33+
34+
public void toggleBubble() {
35+
if (isMinimized()) {
36+
displayAsFloatingWindow();
37+
} else {
38+
displayAsFloatingBubble();
39+
}
40+
}
41+
42+
public void updateLongPressBackgroundResource(boolean isInLongPressState) {
43+
if (isMinimized()) {
44+
return;
45+
}
46+
mTermuxFloatView.setBackgroundResource(isInLongPressState ? R.drawable.floating_window_background_resize : R.drawable.floating_window_background);
47+
}
48+
49+
public void displayAsFloatingBubble() {
50+
captureOriginalLayoutValues();
51+
52+
WindowManager.LayoutParams layoutParams = getLayoutParams();
53+
layoutParams.width = BUBBLE_SIZE;
54+
layoutParams.height = BUBBLE_SIZE;
55+
56+
TerminalView terminalView = getTerminalView();
57+
terminalView.setBackgroundResource(R.drawable.round_button);
58+
terminalView.setClipToOutline(true);
59+
60+
TermuxFloatView termuxFloatView = getTermuxFloatView();
61+
termuxFloatView.setBackgroundResource(R.drawable.round_button_with_outline);
62+
termuxFloatView.setClipToOutline(true);
63+
termuxFloatView.hideTouchKeyboard();
64+
termuxFloatView.changeFocus(false);
65+
66+
ViewGroup windowControls = termuxFloatView.findViewById(R.id.window_controls);
67+
windowControls.setVisibility(View.GONE);
68+
69+
getWindowManager().updateViewLayout(termuxFloatView, layoutParams);
70+
mIsMinimized = true;
71+
}
72+
73+
public void displayAsFloatingWindow() {
74+
WindowManager.LayoutParams layoutParams = getLayoutParams();
75+
76+
// restore back to previous values
77+
layoutParams.width = mOriginalLayoutWidth;
78+
layoutParams.height = mOriginalLayoutHeight;
79+
80+
TerminalView terminalView = getTerminalView();
81+
terminalView.setBackground(mOriginalTerminalViewBackground);
82+
terminalView.setClipToOutline(false);
83+
84+
TermuxFloatView termuxFloatView = getTermuxFloatView();
85+
termuxFloatView.setBackground(mOriginalFloatViewBackground);
86+
termuxFloatView.setClipToOutline(false);
87+
88+
ViewGroup windowControls = termuxFloatView.findViewById(R.id.window_controls);
89+
windowControls.setVisibility(View.VISIBLE);
90+
91+
getWindowManager().updateViewLayout(termuxFloatView, layoutParams);
92+
mIsMinimized = false;
93+
94+
// clear so we can capture proper values on next minimize
95+
mDidCaptureOriginalValues = false;
96+
}
97+
98+
public boolean isMinimized() {
99+
return mIsMinimized;
100+
}
101+
102+
private void captureOriginalLayoutValues() {
103+
if (!mDidCaptureOriginalValues) {
104+
WindowManager.LayoutParams layoutParams = getLayoutParams();
105+
mOriginalLayoutWidth = layoutParams.width;
106+
mOriginalLayoutHeight = layoutParams.height;
107+
108+
mOriginalTerminalViewBackground = getTerminalView().getBackground();
109+
mOriginalFloatViewBackground = getTermuxFloatView().getBackground();
110+
mDidCaptureOriginalValues = true;
111+
}
112+
}
113+
114+
public void cleanup() {
115+
mTermuxFloatView = null;
116+
mOriginalFloatViewBackground = null;
117+
mOriginalTerminalViewBackground = null;
118+
}
119+
120+
private TermuxFloatView getTermuxFloatView() {
121+
return mTermuxFloatView;
122+
}
123+
124+
private TerminalView getTerminalView() {
125+
return mTermuxFloatView.mTerminalView;
126+
}
127+
128+
private WindowManager getWindowManager() {
129+
return mTermuxFloatView.mWindowManager;
130+
}
131+
132+
private WindowManager.LayoutParams getLayoutParams() {
133+
return (WindowManager.LayoutParams) mTermuxFloatView.getLayoutParams();
134+
}
135+
}

app/src/main/java/com/termux/window/TermuxFloatView.java

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import android.annotation.SuppressLint;
44
import android.content.Context;
5+
import android.content.Intent;
56
import android.graphics.PixelFormat;
67
import android.graphics.Point;
78
import android.graphics.Typeface;
@@ -12,8 +13,11 @@
1213
import android.view.MotionEvent;
1314
import android.view.ScaleGestureDetector;
1415
import android.view.ScaleGestureDetector.OnScaleGestureListener;
16+
import android.view.View;
17+
import android.view.ViewGroup;
1518
import android.view.WindowManager;
1619
import android.view.inputmethod.InputMethodManager;
20+
import android.widget.Button;
1721
import android.widget.LinearLayout;
1822
import android.widget.Toast;
1923

@@ -35,6 +39,8 @@ public class TermuxFloatView extends LinearLayout {
3539
InputMethodManager imm;
3640

3741
TerminalView mTerminalView;
42+
ViewGroup mWindowControls;
43+
FloatingBubbleManager mFloatingBubbleManager;
3844

3945
private boolean withFocus = true;
4046
int initialX;
@@ -46,6 +52,8 @@ public class TermuxFloatView extends LinearLayout {
4652

4753
final int[] location = new int[2];
4854

55+
final int[] windowControlsLocation = new int[2];
56+
4957
final ScaleGestureDetector mScaleDetector = new ScaleGestureDetector(getContext(), new OnScaleGestureListener() {
5058
private static final int MIN_SIZE = 50;
5159

@@ -89,6 +97,19 @@ private static int computeLayoutFlags(boolean withFocus) {
8997
public void initializeFloatingWindow() {
9098
mTerminalView = findViewById(R.id.terminal_view);
9199
mTerminalView.setOnKeyListener(new TermuxFloatViewClient(this));
100+
mFloatingBubbleManager = new FloatingBubbleManager(this);
101+
initWindowControls();
102+
}
103+
104+
private void initWindowControls() {
105+
mWindowControls = findViewById(R.id.window_controls);
106+
mWindowControls.setOnClickListener(v -> changeFocus(true));
107+
108+
Button minimizeButton = findViewById(R.id.minimize_button);
109+
minimizeButton.setOnClickListener(v -> mFloatingBubbleManager.toggleBubble());
110+
111+
Button exitButton = findViewById(R.id.exit_button);
112+
exitButton.setOnClickListener(v -> exit());
92113
}
93114

94115
@Override
@@ -147,6 +168,13 @@ public boolean onInterceptTouchEvent(MotionEvent event) {
147168
int y = location[1];
148169
float touchX = event.getRawX();
149170
float touchY = event.getRawY();
171+
172+
if (didClickInsideWindowControls(touchX, touchY)) {
173+
// avoid unintended focus event if we are tapping on our window controls
174+
// so that keyboard doesn't possibly show briefly
175+
return false;
176+
}
177+
150178
boolean clickedInside = (touchX >= x) && (touchX <= (x + layoutParams.width)) && (touchY >= y) && (touchY <= (y + layoutParams.height));
151179

152180
switch (event.getAction()) {
@@ -163,15 +191,31 @@ public boolean onInterceptTouchEvent(MotionEvent event) {
163191
return false;
164192
}
165193

194+
private boolean didClickInsideWindowControls(float touchX, float touchY) {
195+
if (mWindowControls.getVisibility() == View.GONE) {
196+
return false;
197+
}
198+
mWindowControls.getLocationOnScreen(windowControlsLocation);
199+
int controlsX = windowControlsLocation[0];
200+
int controlsY = windowControlsLocation[1];
201+
202+
return (touchX >= controlsX && touchX <= controlsX + mWindowControls.getWidth()) &&
203+
(touchY >= controlsY && touchY <= controlsY + mWindowControls.getHeight());
204+
}
205+
166206
void showTouchKeyboard() {
167207
mTerminalView.post(() -> imm.showSoftInput(mTerminalView, InputMethodManager.SHOW_IMPLICIT));
168208
}
169209

210+
void hideTouchKeyboard() {
211+
mTerminalView.post(() -> imm.hideSoftInputFromWindow(mTerminalView.getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY));
212+
}
213+
170214
void updateLongPressMode(boolean newValue) {
171215
isInLongPressState = newValue;
172-
setBackgroundResource(newValue ? R.drawable.floating_window_background_resize : R.drawable.floating_window_background);
216+
mFloatingBubbleManager.updateLongPressBackgroundResource(isInLongPressState);
173217
setAlpha(newValue ? ALPHA_MOVING : (withFocus ? ALPHA_FOCUS : ALPHA_NOT_FOCUS));
174-
if (newValue) {
218+
if (newValue && !mFloatingBubbleManager.isMinimized()) {
175219
Toast toast = Toast.makeText(getContext(), R.string.after_long_press, Toast.LENGTH_SHORT);
176220
toast.setGravity(Gravity.CENTER, 0, 0);
177221
toast.show();
@@ -207,6 +251,9 @@ public boolean onTouchEvent(MotionEvent event) {
207251
* Visually indicate focus and show the soft input as needed.
208252
*/
209253
void changeFocus(boolean newFocus) {
254+
if (newFocus && mFloatingBubbleManager.isMinimized()) {
255+
mFloatingBubbleManager.displayAsFloatingWindow();
256+
}
210257
if (newFocus == withFocus) {
211258
if (newFocus) showTouchKeyboard();
212259
return;
@@ -219,5 +266,12 @@ void changeFocus(boolean newFocus) {
219266

220267
public void closeFloatingWindow() {
221268
mWindowManager.removeView(this);
269+
mFloatingBubbleManager.cleanup();
270+
mFloatingBubbleManager = null;
271+
}
272+
273+
private void exit() {
274+
Intent intent = new Intent(getContext(), TermuxFloatService.class);
275+
getContext().stopService(intent);
222276
}
223277
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="32dp"
3+
android:height="32dp"
4+
android:viewportWidth="32"
5+
android:viewportHeight="32">
6+
<path
7+
android:pathData="M16.083,31.125c-8.489,0 -15.396,-6.906 -15.396,-15.396S7.594,0.334 16.083,0.334S31.479,7.24 31.479,15.729S24.572,31.125 16.083,31.125zM16.083,2.334c-7.386,0 -13.396,6.009 -13.396,13.396c0,7.387 6.009,13.396 13.396,13.396c7.387,0 13.396,-6.009 13.396,-13.396C29.479,8.343 23.47,2.334 16.083,2.334z"
8+
android:fillColor="#FFFFFF"/>
9+
<path
10+
android:pathData="M17.414,16l7.042,-7.042c0.391,-0.391 0.391,-1.023 0,-1.414s-1.023,-0.391 -1.414,0L16,14.586L8.958,7.544c-0.391,-0.391 -1.023,-0.391 -1.414,0s-0.391,1.023 0,1.414L14.586,16l-7.042,7.042c-0.391,0.391 -0.391,1.023 0,1.414c0.195,0.195 0.451,0.293 0.707,0.293s0.512,-0.098 0.707,-0.293L16,17.414l7.042,7.042c0.195,0.195 0.451,0.293 0.707,0.293s0.512,-0.098 0.707,-0.293c0.391,-0.391 0.391,-1.023 0,-1.414L17.414,16z"
11+
android:fillColor="#FFFFFF"/>
12+
</vector>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="32dp"
3+
android:height="32dp"
4+
android:viewportWidth="32"
5+
android:viewportHeight="32">
6+
<path
7+
android:pathData="M29.625,30.125H16.156c-0.552,0 -1,-0.447 -1,-1s0.448,-1 1,-1h12.469V3.312H3.688v12.632c0,0.552 -0.448,1 -1,1s-1,-0.448 -1,-1V2.312c0,-0.552 0.448,-1 1,-1h26.938c0.553,0 1,0.448 1,1v26.812C30.625,29.678 30.178,30.125 29.625,30.125z"
8+
android:fillColor="#FFFFFF"/>
9+
<path
10+
android:pathData="M8.031,30.625c-3.498,0 -6.344,-2.846 -6.344,-6.344s2.846,-6.344 6.344,-6.344s6.344,2.846 6.344,6.344S11.529,30.625 8.031,30.625zM8.031,19.938c-2.395,0 -4.344,1.948 -4.344,4.344s1.949,4.344 4.344,4.344s4.344,-1.948 4.344,-4.344S10.426,19.938 8.031,19.938z"
11+
android:fillColor="#FFFFFF"/>
12+
<path
13+
android:pathData="M27.051,5.012c-0.391,-0.391 -1.023,-0.391 -1.414,0l-9.199,9.199V9.75c0,-0.552 -0.448,-1 -1,-1s-1,0.448 -1,1V16c0,0.552 0.448,1 1,1h6.5c0.553,0 1,-0.448 1,-1s-0.447,-1 -1,-1h-3.461l8.574,-8.574C27.441,6.035 27.441,5.402 27.051,5.012z"
14+
android:fillColor="#FFFFFF"/>
15+
</vector>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<selector xmlns:android="http://schemas.android.com/apk/res/android">
3+
<item>
4+
<shape android:shape="oval">
5+
<solid android:color="@android:color/black" />
6+
</shape>
7+
</item>
8+
</selector>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<selector xmlns:android="http://schemas.android.com/apk/res/android">
3+
<item>
4+
<shape android:shape="oval">
5+
<solid android:color="@android:color/black" />
6+
<stroke android:color="@android:color/white"
7+
android:width="1dp" />
8+
</shape>
9+
</item>
10+
</selector>

app/src/main/res/layout/activity_main.xml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,38 @@
44
android:layout_width="fill_parent"
55
android:layout_height="fill_parent"
66
android:background="@drawable/floating_window_background"
7+
android:orientation="vertical"
78
android:padding="1px" >
89

10+
<LinearLayout
11+
android:id="@+id/window_controls"
12+
android:layout_width="match_parent"
13+
android:layout_height="wrap_content"
14+
android:background="#ff333333">
15+
<Button
16+
android:id="@+id/minimize_button"
17+
android:layout_width="22dp"
18+
android:layout_height="22dp"
19+
android:background="@drawable/ic_minimize_icon"
20+
android:layout_marginTop="4dp"
21+
android:layout_marginBottom="4dp"
22+
android:layout_marginStart="8dp"
23+
android:layout_marginEnd="8dp" />
24+
<Space
25+
android:layout_width="0dp"
26+
android:layout_height="0dp"
27+
android:layout_weight="1" />
28+
<Button
29+
android:id="@+id/exit_button"
30+
android:layout_width="22dp"
31+
android:layout_height="22dp"
32+
android:background="@drawable/ic_exit_icon"
33+
android:layout_marginTop="4dp"
34+
android:layout_marginBottom="4dp"
35+
android:layout_marginStart="8dp"
36+
android:layout_marginEnd="8dp"/>
37+
</LinearLayout>
38+
939
<com.termux.view.TerminalView
1040
android:id="@+id/terminal_view"
1141
android:layout_width="match_parent"

0 commit comments

Comments
 (0)