fix:android ui v2
Android / build-android (push) Successful in 2m28s

This commit is contained in:
intelligence committed 2026-08-29 16:35:19 +02:00
1 parent e4cf707d0a
commit 8f021959ef
6 files changed
+198 -74

No files matched your search

@@ -2,49 +2,131 @@ package io.github.transui.android;
import android.content.Context; import android.content.Context;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Shader;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View; import android.view.View;
import java.util.Arrays; import java.util.Arrays;
public final class AmplitudeView extends View { public final class AmplitudeView extends View {
private static final int LEVEL_COUNT = 22; private static final int LEVEL_COUNT = 192;
private final float[] levels = new float[LEVEL_COUNT]; private final float[] levels = new float[LEVEL_COUNT];
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint edgePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint centerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Path waveform = new Path();
private final LinearGradient fillGradient;
private final Matrix gradientMatrix = new Matrix();
private final float sampleSpacing;
private final float minimumHalfHeight;
private final int signalColor;
private float previousLevel;
public AmplitudeView(Context context, AttributeSet attributes) { public AmplitudeView(Context context, AttributeSet attributes) {
super(context, attributes); super(context, attributes);
paint.setColor(context.getColor(R.color.signal)); float density = getResources().getDisplayMetrics().density;
paint.setStrokeCap(Paint.Cap.ROUND); sampleSpacing = 5 * density;
minimumHalfHeight = 1.5f * density;
signalColor = context.getColor(R.color.signal);
fillPaint.setStyle(Paint.Style.FILL);
int transparentSignal = signalColor & 0x00ffffff;
fillGradient = new LinearGradient(
0, 0, 0, 1,
new int[]{transparentSignal, signalColor, transparentSignal},
new float[]{0, 0.5f, 1}, Shader.TileMode.CLAMP);
fillPaint.setShader(fillGradient);
edgePaint.setColor(signalColor);
edgePaint.setAlpha(210);
edgePaint.setStyle(Paint.Style.STROKE);
edgePaint.setStrokeWidth(1.5f * density);
edgePaint.setStrokeJoin(Paint.Join.ROUND);
centerPaint.setColor(signalColor);
centerPaint.setAlpha(70);
centerPaint.setStrokeWidth(density);
} }
void setAmplitude(float amplitude) { void setAmplitude(float amplitude) {
float level = Math.max(0, Math.min(1, amplitude));
level = Math.max(level, previousLevel * 0.58f);
System.arraycopy(levels, 1, levels, 0, levels.length - 1); System.arraycopy(levels, 1, levels, 0, levels.length - 1);
levels[levels.length - 1] = Math.max(0, Math.min(1, amplitude)); levels[levels.length - 1] = level;
previousLevel = level;
invalidate(); invalidate();
} }
void reset() { void reset() {
Arrays.fill(levels, 0); Arrays.fill(levels, 0);
previousLevel = 0;
invalidate(); invalidate();
} }
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
super.onSizeChanged(width, height, oldWidth, oldHeight);
float top = getPaddingTop();
float contentHeight = Math.max(1, height - getPaddingTop() - getPaddingBottom());
gradientMatrix.setScale(1, contentHeight);
gradientMatrix.postTranslate(0, top);
fillGradient.setLocalMatrix(gradientMatrix);
}
@Override @Override
protected void onDraw(Canvas canvas) { protected void onDraw(Canvas canvas) {
super.onDraw(canvas); super.onDraw(canvas);
float availableWidth = getWidth() - getPaddingLeft() - getPaddingRight(); float availableWidth = Math.max(0, getWidth() - getPaddingLeft() - getPaddingRight());
float gap = availableWidth / (LEVEL_COUNT * 2 - 1); int visibleCount = Math.min(LEVEL_COUNT,
float strokeWidth = Math.max(2, gap); Math.max(2, (int) (availableWidth / sampleSpacing) + 1));
float step = availableWidth / (visibleCount - 1);
float startX = getPaddingLeft();
float centerY = getHeight() / 2f; float centerY = getHeight() / 2f;
float maximumHeight = Math.max(0, getHeight() - getPaddingTop() - getPaddingBottom()); float maximumHalfHeight = Math.max(minimumHalfHeight,
paint.setStrokeWidth(strokeWidth); (getHeight() - getPaddingTop() - getPaddingBottom()) / 2f);
int firstLevel = LEVEL_COUNT - visibleCount;
for (int index = 0; index < levels.length; index++) { canvas.drawLine(startX, centerY, startX + availableWidth, centerY, centerPaint);
float height = Math.max(strokeWidth, levels[index] * maximumHeight); waveform.reset();
float x = getPaddingLeft() + strokeWidth / 2 + index * gap * 2;
canvas.drawLine(x, centerY - height / 2, x, centerY + height / 2, paint); float previousX = startX;
float previousY = upperY(firstLevel, centerY, maximumHalfHeight);
waveform.moveTo(previousX, previousY);
for (int index = 1; index < visibleCount; index++) {
float x = startX + index * step;
float y = upperY(firstLevel + index, centerY, maximumHalfHeight);
waveform.quadTo(previousX, previousY, (previousX + x) / 2, (previousY + y) / 2);
previousX = x;
previousY = y;
} }
waveform.lineTo(previousX, previousY);
previousY = lowerY(firstLevel + visibleCount - 1, centerY, maximumHalfHeight);
waveform.lineTo(previousX, previousY);
for (int index = visibleCount - 2; index >= 0; index--) {
float x = startX + index * step;
float y = lowerY(firstLevel + index, centerY, maximumHalfHeight);
waveform.quadTo(previousX, previousY, (previousX + x) / 2, (previousY + y) / 2);
previousX = x;
previousY = y;
}
waveform.lineTo(previousX, previousY);
waveform.close();
canvas.drawPath(waveform, fillPaint);
canvas.drawPath(waveform, edgePaint);
}
private float upperY(int index, float centerY, float maximumHalfHeight) {
return centerY - minimumHalfHeight
- levels[index] * (maximumHalfHeight - minimumHalfHeight);
}
private float lowerY(int index, float centerY, float maximumHalfHeight) {
return centerY + minimumHalfHeight
+ levels[index] * (maximumHalfHeight - minimumHalfHeight);
} }
} }
@@ -8,6 +8,7 @@ import android.widget.ImageButton;
public final class PushToTalkButton extends ImageButton { public final class PushToTalkButton extends ImageButton {
private Runnable pressedAction; private Runnable pressedAction;
private Runnable releasedAction; private Runnable releasedAction;
private boolean trackingPress;
public PushToTalkButton(Context context, AttributeSet attributes) { public PushToTalkButton(Context context, AttributeSet attributes) {
super(context, attributes); super(context, attributes);
@@ -16,25 +17,35 @@ public final class PushToTalkButton extends ImageButton {
void setPushToTalkActions(Runnable pressedAction, Runnable releasedAction) { void setPushToTalkActions(Runnable pressedAction, Runnable releasedAction) {
this.pressedAction = pressedAction; this.pressedAction = pressedAction;
this.releasedAction = releasedAction; this.releasedAction = releasedAction;
trackingPress = false;
setPressed(false);
setScaleX(1);
setScaleY(1);
} }
@Override @Override
public boolean onTouchEvent(MotionEvent event) { public boolean onTouchEvent(MotionEvent event) {
if (pressedAction == null || releasedAction == null) return super.onTouchEvent(event); if (pressedAction == null || releasedAction == null) return super.onTouchEvent(event);
if (event.getAction() == MotionEvent.ACTION_DOWN) { if (event.getAction() == MotionEvent.ACTION_DOWN) {
trackingPress = true;
pressedAction.run(); pressedAction.run();
setPressed(true); setPressed(true);
animate().scaleX(0.92f).scaleY(0.92f).setDuration(90).start();
return true; return true;
} }
if (event.getAction() == MotionEvent.ACTION_UP) { if (event.getAction() == MotionEvent.ACTION_UP) {
releasedAction.run(); if (trackingPress) releasedAction.run();
trackingPress = false;
setPressed(false); setPressed(false);
animate().scaleX(1).scaleY(1).setDuration(120).start();
super.performClick(); super.performClick();
return true; return true;
} }
if (event.getAction() == MotionEvent.ACTION_CANCEL) { if (event.getAction() == MotionEvent.ACTION_CANCEL) {
releasedAction.run(); if (trackingPress) releasedAction.run();
trackingPress = false;
setPressed(false); setPressed(false);
animate().scaleX(1).scaleY(1).setDuration(120).start();
return true; return true;
} }
return true; return true;
@@ -42,10 +53,6 @@ public final class PushToTalkButton extends ImageButton {
@Override @Override
public boolean performClick() { public boolean performClick() {
if (pressedAction != null && releasedAction != null) {
pressedAction.run();
releasedAction.run();
}
return super.performClick(); return super.performClick();
} }
} }
@@ -11,6 +11,7 @@ import android.os.Looper;
import android.os.SystemClock; import android.os.SystemClock;
import android.speech.RecognizerIntent; import android.speech.RecognizerIntent;
import android.speech.SpeechRecognizer; import android.speech.SpeechRecognizer;
import android.view.ViewGroup;
import android.widget.TextView; import android.widget.TextView;
import java.util.ArrayList; import java.util.ArrayList;
@@ -79,6 +80,7 @@ public final class SpeechRecognitionActivity extends Activity {
private void configureRecordButton() { private void configureRecordButton() {
record.setAlpha(1); record.setAlpha(1);
resizeRecordButton(config.pushToTalk ? 80 : 64, config.pushToTalk ? 23 : 19);
if (config.pushToTalk) { if (config.pushToTalk) {
record.setImageResource(R.drawable.ic_mic); record.setImageResource(R.drawable.ic_mic);
record.setContentDescription(getString(R.string.hold_to_talk)); record.setContentDescription(getString(R.string.hold_to_talk));
@@ -90,6 +92,17 @@ public final class SpeechRecognitionActivity extends Activity {
} }
} }
private void resizeRecordButton(int sizeDp, int paddingDp) {
float density = getResources().getDisplayMetrics().density;
int size = Math.round(sizeDp * density);
int padding = Math.round(paddingDp * density);
ViewGroup.LayoutParams parameters = record.getLayoutParams();
parameters.width = size;
parameters.height = size;
record.setLayoutParams(parameters);
record.setPadding(padding, padding, padding, padding);
}
private void ensurePermissionAndStart() { private void ensurePermissionAndStart() {
if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
prepareForInput(); prepareForInput();
@@ -181,7 +194,7 @@ public final class SpeechRecognitionActivity extends Activity {
public void onRecording() { public void onRecording() {
runOnUiThread(() -> { runOnUiThread(() -> {
status.setText(R.string.listening); status.setText(R.string.listening);
record.setImageResource(R.drawable.ic_stop); if (!config.pushToTalk) record.setImageResource(R.drawable.ic_stop);
recordingStartedAt = SystemClock.elapsedRealtime(); recordingStartedAt = SystemClock.elapsedRealtime();
elapsed.setText(R.string.elapsed_zero); elapsed.setText(R.string.elapsed_zero);
handler.post(updateElapsed); handler.post(updateElapsed);
@@ -8,6 +8,7 @@ import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.speech.SpeechRecognizer; import android.speech.SpeechRecognizer;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputConnection;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.TextView; import android.widget.TextView;
@@ -22,6 +23,13 @@ public final class TransuiInputMethodService extends InputMethodService {
private VoiceRecordingController controller; private VoiceRecordingController controller;
private boolean sessionActive; private boolean sessionActive;
@Override
public boolean onEvaluateFullscreenMode() {
// Voice input should stay attached to the calling app in landscape. Android's
// default extract editor is the large, blank text area shown above the IME.
return false;
}
@Override @Override
public View onCreateInputView() { public View onCreateInputView() {
FrameLayout parent = new FrameLayout(this); FrameLayout parent = new FrameLayout(this);
@@ -67,6 +75,7 @@ public final class TransuiInputMethodService extends InputMethodService {
record.setAlpha(1); record.setAlpha(1);
record.setOnClickListener(null); record.setOnClickListener(null);
record.setPushToTalkActions(null, null); record.setPushToTalkActions(null, null);
resizeRecordButton(config.pushToTalk ? 76 : 64, config.pushToTalk ? 22 : 19);
if (config.pushToTalk) { if (config.pushToTalk) {
record.setImageResource(R.drawable.ic_mic); record.setImageResource(R.drawable.ic_mic);
record.setContentDescription(getString(R.string.hold_to_talk)); record.setContentDescription(getString(R.string.hold_to_talk));
@@ -78,6 +87,17 @@ public final class TransuiInputMethodService extends InputMethodService {
} }
} }
private void resizeRecordButton(int sizeDp, int paddingDp) {
float density = getResources().getDisplayMetrics().density;
int size = Math.round(sizeDp * density);
int padding = Math.round(paddingDp * density);
ViewGroup.LayoutParams parameters = record.getLayoutParams();
parameters.width = size;
parameters.height = size;
record.setLayoutParams(parameters);
record.setPadding(padding, padding, padding, padding);
}
private void startRecording() { private void startRecording() {
if (sessionActive) return; if (sessionActive) return;
sessionActive = true; sessionActive = true;
@@ -122,7 +142,7 @@ public final class TransuiInputMethodService extends InputMethodService {
public void onRecording() { public void onRecording() {
handler.post(() -> { handler.post(() -> {
status.setText(R.string.listening); status.setText(R.string.listening);
record.setImageResource(R.drawable.ic_stop); if (!config.pushToTalk) record.setImageResource(R.drawable.ic_stop);
}); });
} }
@@ -116,7 +116,8 @@ final class VoiceRecordingController {
} }
ByteArrayOutputStream pcm = new ByteArrayOutputStream(SAMPLE_RATE * 8); ByteArrayOutputStream pcm = new ByteArrayOutputStream(SAMPLE_RATE * 8);
short[] samples = new short[bufferBytes / 2]; // Keep meter updates quick even when AudioRecord asks for a large internal buffer.
short[] samples = new short[Math.min(bufferBytes / 2, 1024)];
long startedAt = SystemClock.elapsedRealtime(); long startedAt = SystemClock.elapsedRealtime();
try { try {
recorder.startRecording(); recorder.startRecording();
@@ -168,6 +169,7 @@ final class VoiceRecordingController {
double rootMeanSquare = Math.sqrt(squaredTotal / count); double rootMeanSquare = Math.sqrt(squaredTotal / count);
if (rootMeanSquare <= 0) return 0; if (rootMeanSquare <= 0) return 0;
double decibels = 20 * Math.log10(rootMeanSquare); double decibels = 20 * Math.log10(rootMeanSquare);
return (float) Math.max(0, Math.min(1, (decibels + 50) / 50)); // Discard the microphone noise floor and reserve the top of the meter for loud speech.
return (float) Math.max(0, Math.min(1, (decibels + 48) / 38));
} }
} }
+50 -50
View File
@@ -17,66 +17,66 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingStart="20dp" android:paddingStart="18dp"
android:paddingTop="12dp" android:paddingTop="10dp"
android:paddingEnd="20dp"> android:paddingEnd="18dp">
<View <View
android:layout_width="4dp" android:layout_width="4dp"
android:layout_height="28dp" android:layout_height="24dp"
android:background="@color/signal" /> android:background="@color/signal" />
<TextView <TextView
android:id="@+id/voice_status" android:id="@+id/voice_status"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:paddingStart="12dp" android:paddingStart="10dp"
android:paddingEnd="12dp" android:paddingEnd="10dp"
android:text="@string/listening" android:text="@string/listening"
android:textColor="@color/ink" android:textColor="@color/ink"
android:textSize="16sp" android:textSize="15sp"
android:textStyle="bold" /> android:textStyle="bold" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="64dp" android:layout_height="86dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingStart="14dp" android:paddingStart="12dp"
android:paddingEnd="14dp" android:paddingEnd="12dp"
android:paddingBottom="8dp"> android:paddingBottom="10dp">
<ImageButton <ImageButton
android:id="@+id/voice_keyboard" android:id="@+id/voice_keyboard"
android:layout_width="48dp" android:layout_width="52dp"
android:layout_height="48dp" android:layout_height="52dp"
android:background="@drawable/voice_secondary_button" android:background="@drawable/voice_secondary_button"
android:contentDescription="@string/keyboard" android:contentDescription="@string/keyboard"
android:padding="12dp" android:padding="13dp"
android:src="@drawable/ic_keyboard" android:src="@drawable/ic_keyboard"
android:tint="@color/ink" /> android:tint="@color/ink" />
<io.github.transui.android.AmplitudeView <io.github.transui.android.AmplitudeView
android:id="@+id/voice_amplitude" android:id="@+id/voice_amplitude"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="44dp" android:layout_height="58dp"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_weight="1" android:layout_weight="1"
android:importantForAccessibility="no" android:importantForAccessibility="no"
android:paddingTop="8dp" android:paddingTop="7dp"
android:paddingBottom="8dp" /> android:paddingBottom="7dp" />
<io.github.transui.android.PushToTalkButton <io.github.transui.android.PushToTalkButton
android:id="@+id/voice_record" android:id="@+id/voice_record"
android:layout_width="48dp" android:layout_width="72dp"
android:layout_height="48dp" android:layout_height="72dp"
android:background="@drawable/voice_primary_button" android:background="@drawable/voice_primary_button"
android:contentDescription="@string/stop" android:contentDescription="@string/stop"
android:padding="12dp" android:padding="21dp"
android:src="@drawable/ic_stop" android:src="@drawable/ic_stop"
android:tint="@color/paper" /> android:tint="@color/paper" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>