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

Implement floating "chat head" bubble #35

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Aug 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions app/src/main/java/com/termux/window/FloatingBubbleManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package com.termux.window;

import android.graphics.drawable.Drawable;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;

import com.termux.view.TerminalView;

/**
* Handles displaying our TermuxFloatView as a collapsed bubble and restoring back
* to its original display.
*/
public class FloatingBubbleManager {
private static final int BUBBLE_SIZE = 200;

private TermuxFloatView mTermuxFloatView;

private boolean mIsMinimized;

// preserve original layout values so we can restore to normal window
// from our bubble
private int mOriginalLayoutWidth;
private int mOriginalLayoutHeight;
private boolean mDidCaptureOriginalValues;
private Drawable mOriginalTerminalViewBackground;
private Drawable mOriginalFloatViewBackground;


public FloatingBubbleManager(TermuxFloatView termuxFloatView) {
mTermuxFloatView = termuxFloatView;
}

public void toggleBubble() {
if (isMinimized()) {
displayAsFloatingWindow();
} else {
displayAsFloatingBubble();
}
}

public void updateLongPressBackgroundResource(boolean isInLongPressState) {
if (isMinimized()) {
return;
}
mTermuxFloatView.setBackgroundResource(isInLongPressState ? R.drawable.floating_window_background_resize : R.drawable.floating_window_background);
}

public void displayAsFloatingBubble() {
captureOriginalLayoutValues();

WindowManager.LayoutParams layoutParams = getLayoutParams();
layoutParams.width = BUBBLE_SIZE;
layoutParams.height = BUBBLE_SIZE;

TerminalView terminalView = getTerminalView();
terminalView.setBackgroundResource(R.drawable.round_button);
terminalView.setClipToOutline(true);

TermuxFloatView termuxFloatView = getTermuxFloatView();
termuxFloatView.setBackgroundResource(R.drawable.round_button_with_outline);
termuxFloatView.setClipToOutline(true);
termuxFloatView.hideTouchKeyboard();
termuxFloatView.changeFocus(false);

ViewGroup windowControls = termuxFloatView.findViewById(R.id.window_controls);
windowControls.setVisibility(View.GONE);

getWindowManager().updateViewLayout(termuxFloatView, layoutParams);
mIsMinimized = true;
}

public void displayAsFloatingWindow() {
WindowManager.LayoutParams layoutParams = getLayoutParams();

// restore back to previous values
layoutParams.width = mOriginalLayoutWidth;
layoutParams.height = mOriginalLayoutHeight;

TerminalView terminalView = getTerminalView();
terminalView.setBackground(mOriginalTerminalViewBackground);
terminalView.setClipToOutline(false);

TermuxFloatView termuxFloatView = getTermuxFloatView();
termuxFloatView.setBackground(mOriginalFloatViewBackground);
termuxFloatView.setClipToOutline(false);

ViewGroup windowControls = termuxFloatView.findViewById(R.id.window_controls);
windowControls.setVisibility(View.VISIBLE);

getWindowManager().updateViewLayout(termuxFloatView, layoutParams);
mIsMinimized = false;

// clear so we can capture proper values on next minimize
mDidCaptureOriginalValues = false;
}

public boolean isMinimized() {
return mIsMinimized;
}

private void captureOriginalLayoutValues() {
if (!mDidCaptureOriginalValues) {
WindowManager.LayoutParams layoutParams = getLayoutParams();
mOriginalLayoutWidth = layoutParams.width;
mOriginalLayoutHeight = layoutParams.height;

mOriginalTerminalViewBackground = getTerminalView().getBackground();
mOriginalFloatViewBackground = getTermuxFloatView().getBackground();
mDidCaptureOriginalValues = true;
}
}

public void cleanup() {
mTermuxFloatView = null;
mOriginalFloatViewBackground = null;
mOriginalTerminalViewBackground = null;
}

private TermuxFloatView getTermuxFloatView() {
return mTermuxFloatView;
}

private TerminalView getTerminalView() {
return mTermuxFloatView.mTerminalView;
}

private WindowManager getWindowManager() {
return mTermuxFloatView.mWindowManager;
}

private WindowManager.LayoutParams getLayoutParams() {
return (WindowManager.LayoutParams) mTermuxFloatView.getLayoutParams();
}
}
58 changes: 56 additions & 2 deletions app/src/main/java/com/termux/window/TermuxFloatView.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Typeface;
Expand All @@ -12,8 +13,11 @@
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.ScaleGestureDetector.OnScaleGestureListener;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.Toast;

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

TerminalView mTerminalView;
ViewGroup mWindowControls;
FloatingBubbleManager mFloatingBubbleManager;

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

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

final int[] windowControlsLocation = new int[2];

final ScaleGestureDetector mScaleDetector = new ScaleGestureDetector(getContext(), new OnScaleGestureListener() {
private static final int MIN_SIZE = 50;

Expand Down Expand Up @@ -89,6 +97,19 @@ private static int computeLayoutFlags(boolean withFocus) {
public void initializeFloatingWindow() {
mTerminalView = findViewById(R.id.terminal_view);
mTerminalView.setOnKeyListener(new TermuxFloatViewClient(this));
mFloatingBubbleManager = new FloatingBubbleManager(this);
initWindowControls();
}

private void initWindowControls() {
mWindowControls = findViewById(R.id.window_controls);
mWindowControls.setOnClickListener(v -> changeFocus(true));

Button minimizeButton = findViewById(R.id.minimize_button);
minimizeButton.setOnClickListener(v -> mFloatingBubbleManager.toggleBubble());

Button exitButton = findViewById(R.id.exit_button);
exitButton.setOnClickListener(v -> exit());
}

@Override
Expand Down Expand Up @@ -147,6 +168,13 @@ public boolean onInterceptTouchEvent(MotionEvent event) {
int y = location[1];
float touchX = event.getRawX();
float touchY = event.getRawY();

if (didClickInsideWindowControls(touchX, touchY)) {
// avoid unintended focus event if we are tapping on our window controls
// so that keyboard doesn't possibly show briefly
return false;
}

boolean clickedInside = (touchX >= x) && (touchX <= (x + layoutParams.width)) && (touchY >= y) && (touchY <= (y + layoutParams.height));

switch (event.getAction()) {
Expand All @@ -163,15 +191,31 @@ public boolean onInterceptTouchEvent(MotionEvent event) {
return false;
}

private boolean didClickInsideWindowControls(float touchX, float touchY) {
if (mWindowControls.getVisibility() == View.GONE) {
return false;
}
mWindowControls.getLocationOnScreen(windowControlsLocation);
int controlsX = windowControlsLocation[0];
int controlsY = windowControlsLocation[1];

return (touchX >= controlsX && touchX <= controlsX + mWindowControls.getWidth()) &&
(touchY >= controlsY && touchY <= controlsY + mWindowControls.getHeight());
}

void showTouchKeyboard() {
mTerminalView.post(() -> imm.showSoftInput(mTerminalView, InputMethodManager.SHOW_IMPLICIT));
}

void hideTouchKeyboard() {
mTerminalView.post(() -> imm.hideSoftInputFromWindow(mTerminalView.getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY));
}

void updateLongPressMode(boolean newValue) {
isInLongPressState = newValue;
setBackgroundResource(newValue ? R.drawable.floating_window_background_resize : R.drawable.floating_window_background);
mFloatingBubbleManager.updateLongPressBackgroundResource(isInLongPressState);
setAlpha(newValue ? ALPHA_MOVING : (withFocus ? ALPHA_FOCUS : ALPHA_NOT_FOCUS));
if (newValue) {
if (newValue && !mFloatingBubbleManager.isMinimized()) {
Toast toast = Toast.makeText(getContext(), R.string.after_long_press, Toast.LENGTH_SHORT);
toast.setGravity(Gravity.CENTER, 0, 0);
toast.show();
Expand Down Expand Up @@ -207,6 +251,9 @@ public boolean onTouchEvent(MotionEvent event) {
* Visually indicate focus and show the soft input as needed.
*/
void changeFocus(boolean newFocus) {
if (newFocus && mFloatingBubbleManager.isMinimized()) {
mFloatingBubbleManager.displayAsFloatingWindow();
}
if (newFocus == withFocus) {
if (newFocus) showTouchKeyboard();
return;
Expand All @@ -219,5 +266,12 @@ void changeFocus(boolean newFocus) {

public void closeFloatingWindow() {
mWindowManager.removeView(this);
mFloatingBubbleManager.cleanup();
mFloatingBubbleManager = null;
}

private void exit() {
Intent intent = new Intent(getContext(), TermuxFloatService.class);
getContext().stopService(intent);
}
}
12 changes: 12 additions & 0 deletions app/src/main/res/drawable/ic_exit_icon.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
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"
android:fillColor="#FFFFFF"/>
<path
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"
android:fillColor="#FFFFFF"/>
</vector>
15 changes: 15 additions & 0 deletions app/src/main/res/drawable/ic_minimize_icon.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
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"
android:fillColor="#FFFFFF"/>
<path
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"
android:fillColor="#FFFFFF"/>
<path
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"
android:fillColor="#FFFFFF"/>
</vector>
8 changes: 8 additions & 0 deletions app/src/main/res/drawable/round_button.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<solid android:color="@android:color/black" />
</shape>
</item>
</selector>
10 changes: 10 additions & 0 deletions app/src/main/res/drawable/round_button_with_outline.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<solid android:color="@android:color/black" />
<stroke android:color="@android:color/white"
android:width="1dp" />
</shape>
</item>
</selector>
30 changes: 30 additions & 0 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,38 @@
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@drawable/floating_window_background"
android:orientation="vertical"
android:padding="1px" >

<LinearLayout
android:id="@+id/window_controls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#ff333333">
<Button
android:id="@+id/minimize_button"
android:layout_width="22dp"
android:layout_height="22dp"
android:background="@drawable/ic_minimize_icon"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" />
<Space
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<Button
android:id="@+id/exit_button"
android:layout_width="22dp"
android:layout_height="22dp"
android:background="@drawable/ic_exit_icon"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"/>
</LinearLayout>

<com.termux.view.TerminalView
android:id="@+id/terminal_view"
android:layout_width="match_parent"
Expand Down