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.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.view.View;
import java.util.Arrays;
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 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) {
super(context, attributes);
paint.setColor(context.getColor(R.color.signal));
paint.setStrokeCap(Paint.Cap.ROUND);
float density = getResources().getDisplayMetrics().density;
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) {
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);
levels[levels.length - 1] = Math.max(0, Math.min(1, amplitude));
levels[levels.length - 1] = level;
previousLevel = level;
invalidate();
}
void reset() {
Arrays.fill(levels, 0);
previousLevel = 0;
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
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float availableWidth = getWidth() - getPaddingLeft() - getPaddingRight();
float gap = availableWidth / (LEVEL_COUNT * 2 - 1);
float strokeWidth = Math.max(2, gap);
float availableWidth = Math.max(0, getWidth() - getPaddingLeft() - getPaddingRight());
int visibleCount = Math.min(LEVEL_COUNT,
Math.max(2, (int) (availableWidth / sampleSpacing) + 1));
float step = availableWidth / (visibleCount - 1);
float startX = getPaddingLeft();
float centerY = getHeight() / 2f;
float maximumHeight = Math.max(0, getHeight() - getPaddingTop() - getPaddingBottom());
paint.setStrokeWidth(strokeWidth);
float maximumHalfHeight = Math.max(minimumHalfHeight,
(getHeight() - getPaddingTop() - getPaddingBottom()) / 2f);
int firstLevel = LEVEL_COUNT - visibleCount;
for (int index = 0; index < levels.length; index++) {
float height = Math.max(strokeWidth, levels[index] * maximumHeight);
float x = getPaddingLeft() + strokeWidth / 2 + index * gap * 2;
canvas.drawLine(x, centerY - height / 2, x, centerY + height / 2, paint);
canvas.drawLine(startX, centerY, startX + availableWidth, centerY, centerPaint);
waveform.reset();
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 {
private Runnable pressedAction;
private Runnable releasedAction;
private boolean trackingPress;
public PushToTalkButton(Context context, AttributeSet attributes) {
super(context, attributes);
@@ -16,25 +17,35 @@ public final class PushToTalkButton extends ImageButton {
void setPushToTalkActions(Runnable pressedAction, Runnable releasedAction) {
this.pressedAction = pressedAction;
this.releasedAction = releasedAction;
trackingPress = false;
setPressed(false);
setScaleX(1);
setScaleY(1);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (pressedAction == null || releasedAction == null) return super.onTouchEvent(event);
if (event.getAction() == MotionEvent.ACTION_DOWN) {
trackingPress = true;
pressedAction.run();
setPressed(true);
animate().scaleX(0.92f).scaleY(0.92f).setDuration(90).start();
return true;
}
if (event.getAction() == MotionEvent.ACTION_UP) {
releasedAction.run();
if (trackingPress) releasedAction.run();
trackingPress = false;
setPressed(false);
animate().scaleX(1).scaleY(1).setDuration(120).start();
super.performClick();
return true;
}
if (event.getAction() == MotionEvent.ACTION_CANCEL) {
releasedAction.run();
if (trackingPress) releasedAction.run();
trackingPress = false;
setPressed(false);
animate().scaleX(1).scaleY(1).setDuration(120).start();
return true;
}
return true;
@@ -42,10 +53,6 @@ public final class PushToTalkButton extends ImageButton {
@Override
public boolean performClick() {
if (pressedAction != null && releasedAction != null) {
pressedAction.run();
releasedAction.run();
}
return super.performClick();
}
}
@@ -11,6 +11,7 @@ import android.os.Looper;
import android.os.SystemClock;
import android.speech.RecognizerIntent;
import android.speech.SpeechRecognizer;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.ArrayList;
@@ -79,6 +80,7 @@ public final class SpeechRecognitionActivity extends Activity {
private void configureRecordButton() {
record.setAlpha(1);
resizeRecordButton(config.pushToTalk ? 80 : 64, config.pushToTalk ? 23 : 19);
if (config.pushToTalk) {
record.setImageResource(R.drawable.ic_mic);
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() {
if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
prepareForInput();
@@ -181,7 +194,7 @@ public final class SpeechRecognitionActivity extends Activity {
public void onRecording() {
runOnUiThread(() -> {
status.setText(R.string.listening);
record.setImageResource(R.drawable.ic_stop);
if (!config.pushToTalk) record.setImageResource(R.drawable.ic_stop);
recordingStartedAt = SystemClock.elapsedRealtime();
elapsed.setText(R.string.elapsed_zero);
handler.post(updateElapsed);
@@ -8,6 +8,7 @@ import android.os.Handler;
import android.os.Looper;
import android.speech.SpeechRecognizer;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputConnection;
import android.widget.FrameLayout;
import android.widget.TextView;
@@ -22,6 +23,13 @@ public final class TransuiInputMethodService extends InputMethodService {
private VoiceRecordingController controller;
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
public View onCreateInputView() {
FrameLayout parent = new FrameLayout(this);
@@ -67,6 +75,7 @@ public final class TransuiInputMethodService extends InputMethodService {
record.setAlpha(1);
record.setOnClickListener(null);
record.setPushToTalkActions(null, null);
resizeRecordButton(config.pushToTalk ? 76 : 64, config.pushToTalk ? 22 : 19);
if (config.pushToTalk) {
record.setImageResource(R.drawable.ic_mic);
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() {
if (sessionActive) return;
sessionActive = true;
@@ -122,7 +142,7 @@ public final class TransuiInputMethodService extends InputMethodService {
public void onRecording() {
handler.post(() -> {
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);
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();
try {
recorder.startRecording();
@@ -168,6 +169,7 @@ final class VoiceRecordingController {
double rootMeanSquare = Math.sqrt(squaredTotal / count);
if (rootMeanSquare <= 0) return 0;
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:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="20dp"
android:paddingTop="12dp"
android:paddingEnd="20dp">
android:paddingStart="18dp"
android:paddingTop="10dp"
android:paddingEnd="18dp">
<View
android:layout_width="4dp"
android:layout_height="28dp"
android:background="@color/signal" />
<View
android:layout_width="4dp"
android:layout_height="24dp"
android:background="@color/signal" />
<TextView
android:id="@+id/voice_status"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:text="@string/listening"
android:textColor="@color/ink"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/voice_status"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:text="@string/listening"
android:textColor="@color/ink"
android:textSize="15sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="64dp"
android:layout_height="86dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="14dp"
android:paddingEnd="14dp"
android:paddingBottom="8dp">
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingBottom="10dp">
<ImageButton
android:id="@+id/voice_keyboard"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/voice_secondary_button"
android:contentDescription="@string/keyboard"
android:padding="12dp"
android:src="@drawable/ic_keyboard"
android:tint="@color/ink" />
<ImageButton
android:id="@+id/voice_keyboard"
android:layout_width="52dp"
android:layout_height="52dp"
android:background="@drawable/voice_secondary_button"
android:contentDescription="@string/keyboard"
android:padding="13dp"
android:src="@drawable/ic_keyboard"
android:tint="@color/ink" />
<io.github.transui.android.AmplitudeView
android:id="@+id/voice_amplitude"
android:layout_width="0dp"
android:layout_height="44dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:importantForAccessibility="no"
android:paddingTop="8dp"
android:paddingBottom="8dp" />
<io.github.transui.android.AmplitudeView
android:id="@+id/voice_amplitude"
android:layout_width="0dp"
android:layout_height="58dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:importantForAccessibility="no"
android:paddingTop="7dp"
android:paddingBottom="7dp" />
<io.github.transui.android.PushToTalkButton
android:id="@+id/voice_record"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/voice_primary_button"
android:contentDescription="@string/stop"
android:padding="12dp"
android:src="@drawable/ic_stop"
android:tint="@color/paper" />
<io.github.transui.android.PushToTalkButton
android:id="@+id/voice_record"
android:layout_width="72dp"
android:layout_height="72dp"
android:background="@drawable/voice_primary_button"
android:contentDescription="@string/stop"
android:padding="21dp"
android:src="@drawable/ic_stop"
android:tint="@color/paper" />
</LinearLayout>
</LinearLayout>