297 lines
13 KiB
Java
297 lines
13 KiB
Java
package com.artemis.fixture;
|
|
|
|
import android.app.Activity;
|
|
import android.graphics.Canvas;
|
|
import android.graphics.Color;
|
|
import android.graphics.Paint;
|
|
import android.os.Bundle;
|
|
import android.text.Editable;
|
|
import android.text.InputType;
|
|
import android.text.TextWatcher;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
import android.view.accessibility.AccessibilityNodeInfo;
|
|
import android.view.inputmethod.EditorInfo;
|
|
import android.widget.Button;
|
|
import android.widget.EditText;
|
|
import android.widget.LinearLayout;
|
|
import android.widget.ScrollView;
|
|
import android.widget.SeekBar;
|
|
import android.widget.TextView;
|
|
|
|
import org.json.JSONArray;
|
|
import org.json.JSONObject;
|
|
|
|
import java.io.FileOutputStream;
|
|
import java.nio.charset.StandardCharsets;
|
|
|
|
/** Disposable, dependency-free native controls and observable touch-event fixture. */
|
|
public final class MainActivity extends Activity {
|
|
private TextView status;
|
|
private TextView touchStatus;
|
|
private EditText editor;
|
|
private SeekBar range;
|
|
private TouchSurface touch;
|
|
private int clicks;
|
|
private int longClicks;
|
|
private int imeEnters;
|
|
private int statusVersion;
|
|
|
|
@Override public void onCreate(Bundle savedInstanceState) {
|
|
super.onCreate(savedInstanceState);
|
|
setTitle("ARTEMIS native actions fixture");
|
|
ScrollView scroll = new ScrollView(this);
|
|
scroll.setContentDescription("fixture.scroll");
|
|
scroll.setFillViewport(true);
|
|
LinearLayout body = new LinearLayout(this);
|
|
body.setOrientation(LinearLayout.VERTICAL);
|
|
body.setPadding(dp(12), dp(12), dp(12), dp(24));
|
|
scroll.addView(body);
|
|
body.addView(label("ARTEMIS native controls", "fixture.title"));
|
|
|
|
status = label("status:ready", "fixture.status");
|
|
body.addView(status);
|
|
range = new SeekBar(this);
|
|
range.setContentDescription("fixture.range");
|
|
range.setMax(100);
|
|
range.setProgress(25);
|
|
body.addView(range, new LinearLayout.LayoutParams(-1, dp(52)));
|
|
range.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
|
@Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
|
updateStatus("range:" + progress);
|
|
}
|
|
@Override public void onStartTrackingTouch(SeekBar seekBar) {}
|
|
@Override public void onStopTrackingTouch(SeekBar seekBar) { writeState(); }
|
|
});
|
|
|
|
Button click = button("Native click", "fixture.click");
|
|
click.setOnClickListener(new View.OnClickListener() {
|
|
@Override public void onClick(View view) { clicks++; updateStatus("click:" + clicks); }
|
|
});
|
|
body.addView(click);
|
|
Button longClick = button("Native long click", "fixture.long_click");
|
|
longClick.setOnLongClickListener(new View.OnLongClickListener() {
|
|
@Override public boolean onLongClick(View view) {
|
|
longClicks++;
|
|
updateStatus("long_click:" + longClicks);
|
|
return true;
|
|
}
|
|
});
|
|
body.addView(longClick);
|
|
Button delayed = button("Change status after 700 ms", "fixture.delayed");
|
|
delayed.setOnClickListener(new View.OnClickListener() {
|
|
@Override public void onClick(View view) {
|
|
updateStatus("status:waiting");
|
|
status.postDelayed(new Runnable() {
|
|
@Override public void run() { statusVersion++; updateStatus("status:changed:" + statusVersion); }
|
|
}, 700L);
|
|
}
|
|
});
|
|
body.addView(delayed);
|
|
|
|
editor = new EditText(this) {
|
|
@Override protected void onSelectionChanged(int start, int end) {
|
|
super.onSelectionChanged(start, end);
|
|
writeState();
|
|
}
|
|
};
|
|
editor.setContentDescription("fixture.editor");
|
|
editor.setInputType(InputType.TYPE_CLASS_TEXT);
|
|
editor.setSingleLine(true);
|
|
editor.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
|
editor.setText("A\uD83D\uDE00B中文");
|
|
editor.setSelection(editor.length());
|
|
editor.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
|
@Override public boolean onEditorAction(TextView view, int actionId, android.view.KeyEvent event) {
|
|
imeEnters++;
|
|
updateStatus("ime_enter:" + imeEnters);
|
|
return true;
|
|
}
|
|
});
|
|
editor.addTextChangedListener(new TextWatcher() {
|
|
@Override public void beforeTextChanged(CharSequence value, int start, int count, int after) {}
|
|
@Override public void onTextChanged(CharSequence value, int start, int before, int count) {}
|
|
@Override public void afterTextChanged(Editable value) { writeState(); }
|
|
});
|
|
body.addView(editor, new LinearLayout.LayoutParams(-1, dp(52)));
|
|
|
|
touchStatus = label("touch:ready", "fixture.touch_status");
|
|
body.addView(touchStatus);
|
|
touch = new TouchSurface();
|
|
body.addView(touch, new LinearLayout.LayoutParams(-1, dp(210)));
|
|
Button reset = button("Reset touch statistics", "fixture.touch_reset");
|
|
reset.setOnClickListener(new View.OnClickListener() {
|
|
@Override public void onClick(View view) { touch.reset(); writeState(); }
|
|
});
|
|
body.addView(reset);
|
|
// Native ScrollView actions must traverse multiple viewports to reach this target.
|
|
for (int i = 0; i < 45; i++) {
|
|
TextView row = label("Native list row " + i, "fixture.row." + i);
|
|
row.setMinHeight(dp(64));
|
|
body.addView(row);
|
|
}
|
|
Button bottom = button("Bottom target", "fixture.bottom");
|
|
bottom.setOnClickListener(new View.OnClickListener() {
|
|
@Override public void onClick(View view) { updateStatus("bottom:clicked"); }
|
|
});
|
|
body.addView(bottom);
|
|
setContentView(scroll);
|
|
writeState();
|
|
}
|
|
|
|
private int dp(int value) { return Math.round(value * getResources().getDisplayMetrics().density); }
|
|
|
|
private TextView label(String text, String description) {
|
|
TextView result = new TextView(this);
|
|
result.setText(text);
|
|
result.setTextSize(16);
|
|
result.setTextColor(Color.BLACK);
|
|
result.setPadding(0, dp(4), 0, dp(4));
|
|
result.setContentDescription(description);
|
|
return result;
|
|
}
|
|
|
|
private Button button(String text, String description) {
|
|
Button result = new Button(this);
|
|
result.setText(text);
|
|
result.setContentDescription(description);
|
|
return result;
|
|
}
|
|
|
|
private void updateStatus(String text) {
|
|
status.setText(text);
|
|
writeState();
|
|
}
|
|
|
|
/** Read explicitly with: adb shell run-as com.artemis.fixture cat files/native_actions_state.json */
|
|
private void writeState() {
|
|
if (editor == null || range == null || touch == null) return;
|
|
try {
|
|
JSONObject state = new JSONObject().put("click_count", clicks).put("long_click_count", longClicks)
|
|
.put("ime_enter_count", imeEnters).put("status", status.getText().toString())
|
|
.put("progress", range.getProgress()).put("text", editor.getText().toString())
|
|
.put("selection_start", editor.getSelectionStart()).put("selection_end", editor.getSelectionEnd())
|
|
.put("touch", touch.state(true));
|
|
try (FileOutputStream stream = openFileOutput("native_actions_state.json", MODE_PRIVATE)) {
|
|
stream.write(state.toString().getBytes(StandardCharsets.UTF_8));
|
|
}
|
|
} catch (Exception ignored) {}
|
|
}
|
|
|
|
private final class TouchSurface extends View {
|
|
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
private JSONArray events = new JSONArray();
|
|
private int downs;
|
|
private int ups;
|
|
private int cancels;
|
|
private int moves;
|
|
private int maxPointers;
|
|
private int pointerDowns;
|
|
private int pointerUps;
|
|
private double initialDistance;
|
|
private double initialAngle;
|
|
private double scale = 1.0;
|
|
private double rotation;
|
|
|
|
TouchSurface() {
|
|
super(MainActivity.this);
|
|
setContentDescription("fixture.touch");
|
|
setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
|
|
setFocusable(true);
|
|
setClickable(true);
|
|
setBackgroundColor(Color.rgb(208, 239, 247));
|
|
paint.setColor(Color.rgb(15, 45, 60));
|
|
paint.setTextSize(dp(16));
|
|
}
|
|
|
|
void reset() {
|
|
events = new JSONArray();
|
|
downs = ups = cancels = moves = maxPointers = pointerDowns = pointerUps = 0;
|
|
initialDistance = initialAngle = rotation = 0;
|
|
scale = 1;
|
|
publish();
|
|
}
|
|
|
|
JSONObject state(boolean includeEvents) throws Exception {
|
|
JSONObject result = new JSONObject().put("down_count", downs).put("up_count", ups)
|
|
.put("cancel_count", cancels).put("move_count", moves).put("max_pointer_count", maxPointers)
|
|
.put("pointer_down_count", pointerDowns).put("pointer_up_count", pointerUps)
|
|
.put("scale", scale).put("rotation_degrees", rotation);
|
|
if (includeEvents) result.put("events", events);
|
|
return result;
|
|
}
|
|
|
|
private void publish() {
|
|
try { touchStatus.setText(state(false).toString()); } catch (Exception ignored) {}
|
|
invalidate();
|
|
writeState();
|
|
}
|
|
|
|
@Override public boolean onTouchEvent(MotionEvent event) {
|
|
int action = event.getActionMasked();
|
|
if (action == MotionEvent.ACTION_DOWN) {
|
|
downs++;
|
|
initialDistance = 0;
|
|
scale = 1;
|
|
rotation = 0;
|
|
getParent().requestDisallowInterceptTouchEvent(true);
|
|
} else if (action == MotionEvent.ACTION_UP) {
|
|
ups++;
|
|
getParent().requestDisallowInterceptTouchEvent(false);
|
|
} else if (action == MotionEvent.ACTION_CANCEL) {
|
|
cancels++;
|
|
getParent().requestDisallowInterceptTouchEvent(false);
|
|
} else if (action == MotionEvent.ACTION_MOVE) {
|
|
moves++;
|
|
} else if (action == MotionEvent.ACTION_POINTER_DOWN) {
|
|
pointerDowns++;
|
|
} else if (action == MotionEvent.ACTION_POINTER_UP) {
|
|
pointerUps++;
|
|
}
|
|
int count = event.getPointerCount();
|
|
maxPointers = Math.max(maxPointers, count);
|
|
if (count >= 2) {
|
|
double dx = event.getX(1) - event.getX(0);
|
|
double dy = event.getY(1) - event.getY(0);
|
|
double distance = Math.hypot(dx, dy);
|
|
double angle = Math.toDegrees(Math.atan2(dy, dx));
|
|
if (initialDistance == 0 || action == MotionEvent.ACTION_POINTER_DOWN) {
|
|
initialDistance = Math.max(distance, 0.001);
|
|
initialAngle = angle;
|
|
}
|
|
scale = distance / initialDistance;
|
|
rotation = angle - initialAngle;
|
|
while (rotation > 180) rotation -= 360;
|
|
while (rotation < -180) rotation += 360;
|
|
}
|
|
try {
|
|
JSONArray pointers = new JSONArray();
|
|
for (int i = 0; i < count; i++) {
|
|
pointers.put(new JSONObject().put("id", event.getPointerId(i))
|
|
.put("x", event.getX(i)).put("y", event.getY(i)));
|
|
}
|
|
if (events.length() >= 64) events.remove(0);
|
|
events.put(new JSONObject().put("action", action).put("time_ms", event.getEventTime())
|
|
.put("pointer_count", count).put("pointers", pointers));
|
|
} catch (Exception ignored) {}
|
|
publish();
|
|
return true;
|
|
}
|
|
|
|
@Override protected void onDraw(Canvas canvas) {
|
|
super.onDraw(canvas);
|
|
canvas.drawText("Touch fixture: pinch, rotate, continuous stroke", dp(8), dp(30), paint);
|
|
canvas.drawText("Down " + downs + " / up " + ups + " / moves " + moves,
|
|
dp(8), dp(60), paint);
|
|
canvas.drawText("Pointers " + maxPointers + " / scale " + String.format(java.util.Locale.ROOT, "%.3f", scale),
|
|
dp(8), dp(90), paint);
|
|
canvas.drawText("Rotation " + String.format(java.util.Locale.ROOT, "%.1f", rotation), dp(8), dp(120), paint);
|
|
}
|
|
|
|
@Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
|
|
super.onInitializeAccessibilityNodeInfo(info);
|
|
info.setClassName("com.artemis.fixture.TouchSurface");
|
|
}
|
|
}
|
|
}
|