Files

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");
}
}
}