Loading tests/TouchLatency/app/build.gradle +2 −2 Original line number Diff line number Diff line apply plugin: 'com.android.application' android { compileSdkVersion 28 compileSdkVersion 33 buildToolsVersion '28.0.3' defaultConfig { applicationId "com.prefabulated.touchlatency" minSdkVersion 28 targetSdkVersion 28 targetSdkVersion 33 versionCode 1 versionName "1.0" } Loading tests/TouchLatency/app/src/main/AndroidManifest.xml +8 −2 Original line number Diff line number Diff line Loading @@ -20,16 +20,22 @@ <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme"> android:theme="@style/AppTheme" android:resizeableActivity="true" > <activity android:name=".TouchLatencyActivity" android:label="@string/app_name" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <activity android:name=".TouchLatencyActivityPresentation" android:label="@string/app_name" android:parentActivityName=".TouchLatencyActivity" android:exported="true"> </activity> </application> </manifest> tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivity.java +65 −206 Original line number Diff line number Diff line Loading @@ -17,218 +17,40 @@ package com.prefabulated.touchlatency; import android.app.Activity; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Paint.Align; import android.app.ActivityOptions; import android.content.Intent; import android.hardware.display.DisplayManager; import android.os.Bundle; import android.os.Handler; import android.os.Trace; import android.util.AttributeSet; import android.util.Log; import android.view.Display; import android.view.Display.Mode; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.Window; import android.view.WindowManager; import java.math.RoundingMode; import java.text.DecimalFormat; class TouchLatencyView extends View implements View.OnTouchListener { private static final String LOG_TAG = "TouchLatency"; private static final int BACKGROUND_COLOR = 0xFF400080; private static final int INNER_RADIUS = 70; private static final int BALL_DIAMETER = 200; private static final int SEC_TO_NANOS = 1000000000; private static final float FPS_UPDATE_THRESHOLD = 20; private static final long BALL_VELOCITY = 420; public TouchLatencyView(Context context, AttributeSet attrs) { super(context, attrs); Trace.beginSection("TouchLatencyView constructor"); setOnTouchListener(this); setWillNotDraw(false); mBluePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mBluePaint.setColor(0xFF0000FF); mBluePaint.setStyle(Paint.Style.FILL); mGreenPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mGreenPaint.setColor(0xFF00FF00); mGreenPaint.setStyle(Paint.Style.FILL); mYellowPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mYellowPaint.setColor(0xFFFFFF00); mYellowPaint.setStyle(Paint.Style.FILL); mRedPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mRedPaint.setColor(0xFFFF0000); mRedPaint.setStyle(Paint.Style.FILL); mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mTextPaint.setColor(0xFFFFFFFF); mTextPaint.setTextSize(100); mTextPaint.setTextAlign(Align.RIGHT); mTouching = false; mLastDrawNano = 0; mFps = 0; mLastFpsUpdate = 0; mFrameCount = 0; mDf = new DecimalFormat("fps: #.##"); mDf.setRoundingMode(RoundingMode.HALF_UP); Trace.endSection(); } public class TouchLatencyActivity extends Activity { private Mode mDisplayModes[]; private int mCurrentModeIndex; private DisplayManager mDisplayManager; private final DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() { @Override public boolean onTouch(View view, MotionEvent event) { Trace.beginSection("TouchLatencyView onTouch"); int action = event.getActionMasked(); if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) { mTouching = true; invalidate(); mTouchX = event.getX(); mTouchY = event.getY(); } else if (action == MotionEvent.ACTION_UP) { mTouching = false; invalidate(); } Trace.endSection(); return true; } private void drawTouch(Canvas canvas) { Trace.beginSection("TouchLatencyView drawTouch"); try { if (!mTouching) { Log.d(LOG_TAG, "Filling background"); canvas.drawColor(BACKGROUND_COLOR); return; } float deltaX = (mTouchX - mLastDrawnX); float deltaY = (mTouchY - mLastDrawnY); float scaleFactor = (float) Math.sqrt(deltaX * deltaX + deltaY * deltaY) * 1.5f; mLastDrawnX = mTouchX; mLastDrawnY = mTouchY; canvas.drawColor(BACKGROUND_COLOR); canvas.drawCircle(mTouchX, mTouchY, INNER_RADIUS + 3 * scaleFactor, mRedPaint); canvas.drawCircle(mTouchX, mTouchY, INNER_RADIUS + 2 * scaleFactor, mYellowPaint); canvas.drawCircle(mTouchX, mTouchY, INNER_RADIUS + scaleFactor, mGreenPaint); canvas.drawCircle(mTouchX, mTouchY, INNER_RADIUS, mBluePaint); } finally { Trace.endSection(); } } private Paint getBallColor() { if (mFps > 75) return mGreenPaint; else if (mFps > 45) return mYellowPaint; else return mRedPaint; } private void drawBall(Canvas canvas) { Trace.beginSection("TouchLatencyView drawBall"); int width = canvas.getWidth(); int height = canvas.getHeight(); float fps = 0f; long t = System.nanoTime(); long tDiff = t - mLastDrawNano; mLastDrawNano = t; mFrameCount++; if (tDiff < SEC_TO_NANOS) { fps = 1f * SEC_TO_NANOS / tDiff; } long fDiff = t - mLastFpsUpdate; if (Math.abs(mFps - fps) > FPS_UPDATE_THRESHOLD) { mFps = fps; mLastFpsUpdate = t; mFrameCount = 0; } else if (fDiff > SEC_TO_NANOS) { mFps = 1f * mFrameCount * SEC_TO_NANOS / fDiff; mLastFpsUpdate = t; mFrameCount = 0; } final long pos = t * BALL_VELOCITY / SEC_TO_NANOS; final long xMax = width - BALL_DIAMETER; final long yMax = height - BALL_DIAMETER; long xOffset = pos % xMax; long yOffset = pos % yMax; float left, right, top, bottom; if (((pos / xMax) & 1) == 0) { left = xMax - xOffset; } else { left = xOffset; } right = left + BALL_DIAMETER; if (((pos / yMax) & 1) == 0) { top = yMax - yOffset; } else { top = yOffset; } bottom = top + BALL_DIAMETER; // Draw the ball canvas.drawColor(BACKGROUND_COLOR); canvas.drawOval(left, top, right, bottom, getBallColor()); canvas.drawText(mDf.format(mFps), width, 100, mTextPaint); invalidate(); Trace.endSection(); public void onDisplayAdded(int i) { invalidateOptionsMenu(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Trace.beginSection("TouchLatencyView onDraw"); if (mMode == 0) { drawTouch(canvas); } else { drawBall(canvas); } Trace.endSection(); } public void changeMode(MenuItem item) { Trace.beginSection("TouchLatencyView changeMode"); final int NUM_MODES = 2; final String modes[] = {"Touch", "Ball"}; mMode = (mMode + 1) % NUM_MODES; invalidate(); item.setTitle(modes[mMode]); Trace.endSection(); public void onDisplayRemoved(int i) { invalidateOptionsMenu(); } private final Paint mBluePaint, mGreenPaint, mYellowPaint, mRedPaint, mTextPaint; private int mMode; private boolean mTouching; private float mTouchX, mTouchY; private float mLastDrawnX, mLastDrawnY; private long mLastDrawNano, mLastFpsUpdate, mFrameCount; private float mFps; private DecimalFormat mDf; @Override public void onDisplayChanged(int i) { invalidateOptionsMenu(); } public class TouchLatencyActivity extends Activity { private Mode mDisplayModes[]; private int mCurrentModeIndex; }; @Override protected void onCreate(Bundle savedInstanceState) { Loading @@ -236,9 +58,9 @@ public class TouchLatencyActivity extends Activity { Trace.beginSection("TouchLatencyActivity onCreate"); setContentView(R.layout.activity_touch_latency); mTouchView = findViewById(R.id.canvasView); configureDisplayListener(); WindowManager wm = getWindowManager(); Display display = wm.getDefaultDisplay(); mDisplayModes = display.getSupportedModes(); Loading @@ -250,11 +72,9 @@ public class TouchLatencyActivity extends Activity { break; } } Trace.endSection(); } @Override public boolean onCreateOptionsMenu(Menu menu) { Trace.beginSection("TouchLatencyActivity onCreateOptionsMenu"); Loading @@ -265,17 +85,26 @@ public class TouchLatencyActivity extends Activity { Mode currentMode = getWindowManager().getDefaultDisplay().getMode(); updateDisplayMode(menuItem, currentMode); } updateMultiDisplayMenu(menu.findItem(R.id.multi_display)); Trace.endSection(); return true; } private void updateDisplayMode(MenuItem menuItem, Mode displayMode) { int fps = (int) displayMode.getRefreshRate(); menuItem.setTitle(fps + "hz"); menuItem.setVisible(true); } private void updateMultiDisplayMenu(MenuItem item) { item.setVisible(mDisplayManager.getDisplays().length > 1); } private void configureDisplayListener() { mDisplayManager = getSystemService(DisplayManager.class); mDisplayManager.registerDisplayListener(mDisplayListener, new Handler()); } public void changeDisplayMode(MenuItem item) { Window w = getWindow(); WindowManager.LayoutParams params = w.getAttributes(); Loading @@ -299,6 +128,19 @@ public class TouchLatencyActivity extends Activity { mCurrentModeIndex = modeIndex; } private void changeMultipleDisplays() { Intent intent = new Intent(this, TouchLatencyActivityPresentation.class); intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT | Intent.FLAG_ACTIVITY_NEW_TASK); ActivityOptions options = ActivityOptions.makeBasic(); for (int i = 1; i < mDisplayManager.getDisplays().length; ++i) { // We assume the first display is already displaying the TouchLatencyActivity int displayId = mDisplayManager.getDisplays()[i].getDisplayId(); options.setLaunchDisplayId(displayId); intent.putExtra(TouchLatencyActivityPresentation.DISPLAY_ID, displayId); startActivity(intent, options.toBundle()); } } @Override public boolean onOptionsItemSelected(MenuItem item) { Loading @@ -309,15 +151,32 @@ public class TouchLatencyActivity extends Activity { int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.action_settings) { switch (id) { case R.id.action_settings: { mTouchView.changeMode(item); } else if (id == R.id.display_mode) { break; } case R.id.display_mode: { changeDisplayMode(item); break; } case R.id.multi_display: { changeMultipleDisplays(); break; } } Trace.endSection(); return super.onOptionsItemSelected(item); } @Override protected void onDestroy() { super.onDestroy(); if (mDisplayManager != null) { mDisplayManager.unregisterDisplayListener(mDisplayListener); } } private TouchLatencyView mTouchView; } tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivityPresentation.java 0 → 100644 +128 −0 Original line number Diff line number Diff line /* * Copyright 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.prefabulated.touchlatency; import android.app.Activity; import android.os.Bundle; import android.os.Trace; import android.view.Display; import android.view.Display.Mode; import android.view.Menu; import android.view.MenuItem; import android.view.Window; import android.view.WindowManager; public class TouchLatencyActivityPresentation extends Activity { public static final String DISPLAY_ID = "DISPLAY_ID"; private Mode[] mDisplayModes; private int mCurrentModeIndex; private int mDisplayId; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getIntent().hasExtra(DISPLAY_ID)) { mDisplayId = (int) getIntent().getExtras().get(DISPLAY_ID); } Trace.beginSection( "TouchLatencyActivityPresentation::DisplayId::" + mDisplayId + " onCreate"); setContentView(R.layout.activity_touch_latency); mTouchView = findViewById(R.id.canvasView); WindowManager wm = getWindowManager(); Display display = wm.getDefaultDisplay(); mDisplayModes = display.getSupportedModes(); Mode currentMode = getWindowManager().getDefaultDisplay().getMode(); for (int i = 0; i < mDisplayModes.length; i++) { if (currentMode.getModeId() == mDisplayModes[i].getModeId()) { mCurrentModeIndex = i; break; } } Trace.endSection(); } @Override protected void onResume() { super.onResume(); } @Override public boolean onCreateOptionsMenu(Menu menu) { Trace.beginSection( "TouchLatencyActivityPresentation::DisplayId:: " + mDisplayId + " onCreateOptionsMenu"); // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_touch_latency, menu); if (mDisplayModes.length > 1) { MenuItem menuItem = menu.findItem(R.id.display_mode); Mode currentMode = getWindowManager().getDefaultDisplay().getMode(); updateDisplayMode(menuItem, currentMode); } Trace.endSection(); return true; } private void updateDisplayMode(MenuItem menuItem, Mode displayMode) { int fps = (int) displayMode.getRefreshRate(); menuItem.setTitle(fps + "hz"); menuItem.setVisible(true); } public void changeDisplayMode(MenuItem item) { Window w = getWindow(); WindowManager.LayoutParams params = w.getAttributes(); int modeIndex = (mCurrentModeIndex + 1) % mDisplayModes.length; params.preferredDisplayModeId = mDisplayModes[modeIndex].getModeId(); w.setAttributes(params); updateDisplayMode(item, mDisplayModes[modeIndex]); mCurrentModeIndex = modeIndex; } @Override public boolean onOptionsItemSelected(MenuItem item) { Trace.beginSection( "TouchLatencyActivityPresentation::DisplayId::" + mDisplayId + " onOptionsItemSelected"); // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); //noinspection SimplifiableIfStatement switch (id) { case R.id.action_settings: { mTouchView.changeMode(item); break; } case R.id.display_mode: { changeDisplayMode(item); break; } } Trace.endSection(); return super.onOptionsItemSelected(item); } private TouchLatencyView mTouchView; } tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyView.java 0 → 100644 +219 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.prefabulated.touchlatency; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.os.Trace; import android.util.AttributeSet; import android.util.Log; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import java.math.RoundingMode; import java.text.DecimalFormat; class TouchLatencyView extends View implements View.OnTouchListener { private static final String LOG_TAG = "TouchLatency"; private static final int BACKGROUND_COLOR = 0xFF400080; private static final int INNER_RADIUS = 70; private static final int BALL_DIAMETER = 200; private static final int SEC_TO_NANOS = 1000000000; private static final float FPS_UPDATE_THRESHOLD = 20; private static final long BALL_VELOCITY = 420; public TouchLatencyView(Context context, AttributeSet attrs) { super(context, attrs); Trace.beginSection("TouchLatencyView constructor"); setOnTouchListener(this); setWillNotDraw(false); mBluePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mBluePaint.setColor(0xFF0000FF); mBluePaint.setStyle(Paint.Style.FILL); mGreenPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mGreenPaint.setColor(0xFF00FF00); mGreenPaint.setStyle(Paint.Style.FILL); mYellowPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mYellowPaint.setColor(0xFFFFFF00); mYellowPaint.setStyle(Paint.Style.FILL); mRedPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mRedPaint.setColor(0xFFFF0000); mRedPaint.setStyle(Paint.Style.FILL); mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mTextPaint.setColor(0xFFFFFFFF); mTextPaint.setTextSize(100); mTextPaint.setTextAlign(Paint.Align.RIGHT); mTouching = false; mLastDrawNano = 0; mFps = 0; mLastFpsUpdate = 0; mFrameCount = 0; mDf = new DecimalFormat("fps: #.##"); mDf.setRoundingMode(RoundingMode.HALF_UP); Trace.endSection(); } @Override public boolean onTouch(View view, MotionEvent event) { Trace.beginSection("TouchLatencyView onTouch"); int action = event.getActionMasked(); if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) { mTouching = true; invalidate(); mTouchX = event.getX(); mTouchY = event.getY(); } else if (action == MotionEvent.ACTION_UP) { mTouching = false; invalidate(); } Trace.endSection(); return true; } private void drawTouch(Canvas canvas) { Trace.beginSection("TouchLatencyView drawTouch"); try { if (!mTouching) { Log.d(LOG_TAG, "Filling background"); canvas.drawColor(BACKGROUND_COLOR); return; } float deltaX = (mTouchX - mLastDrawnX); float deltaY = (mTouchY - mLastDrawnY); float scaleFactor = (float) Math.sqrt(deltaX * deltaX + deltaY * deltaY) * 1.5f; mLastDrawnX = mTouchX; mLastDrawnY = mTouchY; canvas.drawColor(BACKGROUND_COLOR); canvas.drawCircle(mTouchX, mTouchY, INNER_RADIUS + 3 * scaleFactor, mRedPaint); canvas.drawCircle(mTouchX, mTouchY, INNER_RADIUS + 2 * scaleFactor, mYellowPaint); canvas.drawCircle(mTouchX, mTouchY, INNER_RADIUS + scaleFactor, mGreenPaint); canvas.drawCircle(mTouchX, mTouchY, INNER_RADIUS, mBluePaint); } finally { Trace.endSection(); } } private Paint getBallColor() { if (mFps > 75) { return mGreenPaint; } else if (mFps > 45) { return mYellowPaint; } else return mRedPaint; } private void drawBall(Canvas canvas) { Trace.beginSection("TouchLatencyView drawBall"); int width = canvas.getWidth(); int height = canvas.getHeight(); float fps = 0f; long t = System.nanoTime(); long tDiff = t - mLastDrawNano; mLastDrawNano = t; mFrameCount++; if (tDiff < SEC_TO_NANOS) { fps = 1f * SEC_TO_NANOS / tDiff; } long fDiff = t - mLastFpsUpdate; if (Math.abs(mFps - fps) > FPS_UPDATE_THRESHOLD) { mFps = fps; mLastFpsUpdate = t; mFrameCount = 0; } else if (fDiff > SEC_TO_NANOS) { mFps = 1f * mFrameCount * SEC_TO_NANOS / fDiff; mLastFpsUpdate = t; mFrameCount = 0; } final long pos = t * BALL_VELOCITY / SEC_TO_NANOS; final long xMax = width - BALL_DIAMETER; final long yMax = height - BALL_DIAMETER; long xOffset = pos % xMax; long yOffset = pos % yMax; float left, right, top, bottom; if (((pos / xMax) & 1) == 0) { left = xMax - xOffset; } else { left = xOffset; } right = left + BALL_DIAMETER; if (((pos / yMax) & 1) == 0) { top = yMax - yOffset; } else { top = yOffset; } bottom = top + BALL_DIAMETER; // Draw the ball canvas.drawColor(BACKGROUND_COLOR); canvas.drawOval(left, top, right, bottom, getBallColor()); canvas.drawText(mDf.format(mFps), width, 100, mTextPaint); invalidate(); Trace.endSection(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Trace.beginSection("TouchLatencyView onDraw"); if (mMode == 0) { drawTouch(canvas); } else { drawBall(canvas); } Trace.endSection(); } public void changeMode(MenuItem item) { Trace.beginSection("TouchLatencyView changeMode"); final int NUM_MODES = 2; final String modes[] = {"Touch", "Ball"}; mMode = (mMode + 1) % NUM_MODES; invalidate(); item.setTitle(modes[mMode]); Trace.endSection(); } private final Paint mBluePaint, mGreenPaint, mYellowPaint, mRedPaint, mTextPaint; private int mMode; private boolean mTouching; private float mTouchX, mTouchY; private float mLastDrawnX, mLastDrawnY; private long mLastDrawNano, mLastFpsUpdate, mFrameCount; private float mFps; private DecimalFormat mDf; } Loading
tests/TouchLatency/app/build.gradle +2 −2 Original line number Diff line number Diff line apply plugin: 'com.android.application' android { compileSdkVersion 28 compileSdkVersion 33 buildToolsVersion '28.0.3' defaultConfig { applicationId "com.prefabulated.touchlatency" minSdkVersion 28 targetSdkVersion 28 targetSdkVersion 33 versionCode 1 versionName "1.0" } Loading
tests/TouchLatency/app/src/main/AndroidManifest.xml +8 −2 Original line number Diff line number Diff line Loading @@ -20,16 +20,22 @@ <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme"> android:theme="@style/AppTheme" android:resizeableActivity="true" > <activity android:name=".TouchLatencyActivity" android:label="@string/app_name" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <activity android:name=".TouchLatencyActivityPresentation" android:label="@string/app_name" android:parentActivityName=".TouchLatencyActivity" android:exported="true"> </activity> </application> </manifest>
tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivity.java +65 −206 Original line number Diff line number Diff line Loading @@ -17,218 +17,40 @@ package com.prefabulated.touchlatency; import android.app.Activity; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Paint.Align; import android.app.ActivityOptions; import android.content.Intent; import android.hardware.display.DisplayManager; import android.os.Bundle; import android.os.Handler; import android.os.Trace; import android.util.AttributeSet; import android.util.Log; import android.view.Display; import android.view.Display.Mode; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.Window; import android.view.WindowManager; import java.math.RoundingMode; import java.text.DecimalFormat; class TouchLatencyView extends View implements View.OnTouchListener { private static final String LOG_TAG = "TouchLatency"; private static final int BACKGROUND_COLOR = 0xFF400080; private static final int INNER_RADIUS = 70; private static final int BALL_DIAMETER = 200; private static final int SEC_TO_NANOS = 1000000000; private static final float FPS_UPDATE_THRESHOLD = 20; private static final long BALL_VELOCITY = 420; public TouchLatencyView(Context context, AttributeSet attrs) { super(context, attrs); Trace.beginSection("TouchLatencyView constructor"); setOnTouchListener(this); setWillNotDraw(false); mBluePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mBluePaint.setColor(0xFF0000FF); mBluePaint.setStyle(Paint.Style.FILL); mGreenPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mGreenPaint.setColor(0xFF00FF00); mGreenPaint.setStyle(Paint.Style.FILL); mYellowPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mYellowPaint.setColor(0xFFFFFF00); mYellowPaint.setStyle(Paint.Style.FILL); mRedPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mRedPaint.setColor(0xFFFF0000); mRedPaint.setStyle(Paint.Style.FILL); mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mTextPaint.setColor(0xFFFFFFFF); mTextPaint.setTextSize(100); mTextPaint.setTextAlign(Align.RIGHT); mTouching = false; mLastDrawNano = 0; mFps = 0; mLastFpsUpdate = 0; mFrameCount = 0; mDf = new DecimalFormat("fps: #.##"); mDf.setRoundingMode(RoundingMode.HALF_UP); Trace.endSection(); } public class TouchLatencyActivity extends Activity { private Mode mDisplayModes[]; private int mCurrentModeIndex; private DisplayManager mDisplayManager; private final DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() { @Override public boolean onTouch(View view, MotionEvent event) { Trace.beginSection("TouchLatencyView onTouch"); int action = event.getActionMasked(); if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) { mTouching = true; invalidate(); mTouchX = event.getX(); mTouchY = event.getY(); } else if (action == MotionEvent.ACTION_UP) { mTouching = false; invalidate(); } Trace.endSection(); return true; } private void drawTouch(Canvas canvas) { Trace.beginSection("TouchLatencyView drawTouch"); try { if (!mTouching) { Log.d(LOG_TAG, "Filling background"); canvas.drawColor(BACKGROUND_COLOR); return; } float deltaX = (mTouchX - mLastDrawnX); float deltaY = (mTouchY - mLastDrawnY); float scaleFactor = (float) Math.sqrt(deltaX * deltaX + deltaY * deltaY) * 1.5f; mLastDrawnX = mTouchX; mLastDrawnY = mTouchY; canvas.drawColor(BACKGROUND_COLOR); canvas.drawCircle(mTouchX, mTouchY, INNER_RADIUS + 3 * scaleFactor, mRedPaint); canvas.drawCircle(mTouchX, mTouchY, INNER_RADIUS + 2 * scaleFactor, mYellowPaint); canvas.drawCircle(mTouchX, mTouchY, INNER_RADIUS + scaleFactor, mGreenPaint); canvas.drawCircle(mTouchX, mTouchY, INNER_RADIUS, mBluePaint); } finally { Trace.endSection(); } } private Paint getBallColor() { if (mFps > 75) return mGreenPaint; else if (mFps > 45) return mYellowPaint; else return mRedPaint; } private void drawBall(Canvas canvas) { Trace.beginSection("TouchLatencyView drawBall"); int width = canvas.getWidth(); int height = canvas.getHeight(); float fps = 0f; long t = System.nanoTime(); long tDiff = t - mLastDrawNano; mLastDrawNano = t; mFrameCount++; if (tDiff < SEC_TO_NANOS) { fps = 1f * SEC_TO_NANOS / tDiff; } long fDiff = t - mLastFpsUpdate; if (Math.abs(mFps - fps) > FPS_UPDATE_THRESHOLD) { mFps = fps; mLastFpsUpdate = t; mFrameCount = 0; } else if (fDiff > SEC_TO_NANOS) { mFps = 1f * mFrameCount * SEC_TO_NANOS / fDiff; mLastFpsUpdate = t; mFrameCount = 0; } final long pos = t * BALL_VELOCITY / SEC_TO_NANOS; final long xMax = width - BALL_DIAMETER; final long yMax = height - BALL_DIAMETER; long xOffset = pos % xMax; long yOffset = pos % yMax; float left, right, top, bottom; if (((pos / xMax) & 1) == 0) { left = xMax - xOffset; } else { left = xOffset; } right = left + BALL_DIAMETER; if (((pos / yMax) & 1) == 0) { top = yMax - yOffset; } else { top = yOffset; } bottom = top + BALL_DIAMETER; // Draw the ball canvas.drawColor(BACKGROUND_COLOR); canvas.drawOval(left, top, right, bottom, getBallColor()); canvas.drawText(mDf.format(mFps), width, 100, mTextPaint); invalidate(); Trace.endSection(); public void onDisplayAdded(int i) { invalidateOptionsMenu(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Trace.beginSection("TouchLatencyView onDraw"); if (mMode == 0) { drawTouch(canvas); } else { drawBall(canvas); } Trace.endSection(); } public void changeMode(MenuItem item) { Trace.beginSection("TouchLatencyView changeMode"); final int NUM_MODES = 2; final String modes[] = {"Touch", "Ball"}; mMode = (mMode + 1) % NUM_MODES; invalidate(); item.setTitle(modes[mMode]); Trace.endSection(); public void onDisplayRemoved(int i) { invalidateOptionsMenu(); } private final Paint mBluePaint, mGreenPaint, mYellowPaint, mRedPaint, mTextPaint; private int mMode; private boolean mTouching; private float mTouchX, mTouchY; private float mLastDrawnX, mLastDrawnY; private long mLastDrawNano, mLastFpsUpdate, mFrameCount; private float mFps; private DecimalFormat mDf; @Override public void onDisplayChanged(int i) { invalidateOptionsMenu(); } public class TouchLatencyActivity extends Activity { private Mode mDisplayModes[]; private int mCurrentModeIndex; }; @Override protected void onCreate(Bundle savedInstanceState) { Loading @@ -236,9 +58,9 @@ public class TouchLatencyActivity extends Activity { Trace.beginSection("TouchLatencyActivity onCreate"); setContentView(R.layout.activity_touch_latency); mTouchView = findViewById(R.id.canvasView); configureDisplayListener(); WindowManager wm = getWindowManager(); Display display = wm.getDefaultDisplay(); mDisplayModes = display.getSupportedModes(); Loading @@ -250,11 +72,9 @@ public class TouchLatencyActivity extends Activity { break; } } Trace.endSection(); } @Override public boolean onCreateOptionsMenu(Menu menu) { Trace.beginSection("TouchLatencyActivity onCreateOptionsMenu"); Loading @@ -265,17 +85,26 @@ public class TouchLatencyActivity extends Activity { Mode currentMode = getWindowManager().getDefaultDisplay().getMode(); updateDisplayMode(menuItem, currentMode); } updateMultiDisplayMenu(menu.findItem(R.id.multi_display)); Trace.endSection(); return true; } private void updateDisplayMode(MenuItem menuItem, Mode displayMode) { int fps = (int) displayMode.getRefreshRate(); menuItem.setTitle(fps + "hz"); menuItem.setVisible(true); } private void updateMultiDisplayMenu(MenuItem item) { item.setVisible(mDisplayManager.getDisplays().length > 1); } private void configureDisplayListener() { mDisplayManager = getSystemService(DisplayManager.class); mDisplayManager.registerDisplayListener(mDisplayListener, new Handler()); } public void changeDisplayMode(MenuItem item) { Window w = getWindow(); WindowManager.LayoutParams params = w.getAttributes(); Loading @@ -299,6 +128,19 @@ public class TouchLatencyActivity extends Activity { mCurrentModeIndex = modeIndex; } private void changeMultipleDisplays() { Intent intent = new Intent(this, TouchLatencyActivityPresentation.class); intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT | Intent.FLAG_ACTIVITY_NEW_TASK); ActivityOptions options = ActivityOptions.makeBasic(); for (int i = 1; i < mDisplayManager.getDisplays().length; ++i) { // We assume the first display is already displaying the TouchLatencyActivity int displayId = mDisplayManager.getDisplays()[i].getDisplayId(); options.setLaunchDisplayId(displayId); intent.putExtra(TouchLatencyActivityPresentation.DISPLAY_ID, displayId); startActivity(intent, options.toBundle()); } } @Override public boolean onOptionsItemSelected(MenuItem item) { Loading @@ -309,15 +151,32 @@ public class TouchLatencyActivity extends Activity { int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.action_settings) { switch (id) { case R.id.action_settings: { mTouchView.changeMode(item); } else if (id == R.id.display_mode) { break; } case R.id.display_mode: { changeDisplayMode(item); break; } case R.id.multi_display: { changeMultipleDisplays(); break; } } Trace.endSection(); return super.onOptionsItemSelected(item); } @Override protected void onDestroy() { super.onDestroy(); if (mDisplayManager != null) { mDisplayManager.unregisterDisplayListener(mDisplayListener); } } private TouchLatencyView mTouchView; }
tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivityPresentation.java 0 → 100644 +128 −0 Original line number Diff line number Diff line /* * Copyright 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.prefabulated.touchlatency; import android.app.Activity; import android.os.Bundle; import android.os.Trace; import android.view.Display; import android.view.Display.Mode; import android.view.Menu; import android.view.MenuItem; import android.view.Window; import android.view.WindowManager; public class TouchLatencyActivityPresentation extends Activity { public static final String DISPLAY_ID = "DISPLAY_ID"; private Mode[] mDisplayModes; private int mCurrentModeIndex; private int mDisplayId; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getIntent().hasExtra(DISPLAY_ID)) { mDisplayId = (int) getIntent().getExtras().get(DISPLAY_ID); } Trace.beginSection( "TouchLatencyActivityPresentation::DisplayId::" + mDisplayId + " onCreate"); setContentView(R.layout.activity_touch_latency); mTouchView = findViewById(R.id.canvasView); WindowManager wm = getWindowManager(); Display display = wm.getDefaultDisplay(); mDisplayModes = display.getSupportedModes(); Mode currentMode = getWindowManager().getDefaultDisplay().getMode(); for (int i = 0; i < mDisplayModes.length; i++) { if (currentMode.getModeId() == mDisplayModes[i].getModeId()) { mCurrentModeIndex = i; break; } } Trace.endSection(); } @Override protected void onResume() { super.onResume(); } @Override public boolean onCreateOptionsMenu(Menu menu) { Trace.beginSection( "TouchLatencyActivityPresentation::DisplayId:: " + mDisplayId + " onCreateOptionsMenu"); // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_touch_latency, menu); if (mDisplayModes.length > 1) { MenuItem menuItem = menu.findItem(R.id.display_mode); Mode currentMode = getWindowManager().getDefaultDisplay().getMode(); updateDisplayMode(menuItem, currentMode); } Trace.endSection(); return true; } private void updateDisplayMode(MenuItem menuItem, Mode displayMode) { int fps = (int) displayMode.getRefreshRate(); menuItem.setTitle(fps + "hz"); menuItem.setVisible(true); } public void changeDisplayMode(MenuItem item) { Window w = getWindow(); WindowManager.LayoutParams params = w.getAttributes(); int modeIndex = (mCurrentModeIndex + 1) % mDisplayModes.length; params.preferredDisplayModeId = mDisplayModes[modeIndex].getModeId(); w.setAttributes(params); updateDisplayMode(item, mDisplayModes[modeIndex]); mCurrentModeIndex = modeIndex; } @Override public boolean onOptionsItemSelected(MenuItem item) { Trace.beginSection( "TouchLatencyActivityPresentation::DisplayId::" + mDisplayId + " onOptionsItemSelected"); // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); //noinspection SimplifiableIfStatement switch (id) { case R.id.action_settings: { mTouchView.changeMode(item); break; } case R.id.display_mode: { changeDisplayMode(item); break; } } Trace.endSection(); return super.onOptionsItemSelected(item); } private TouchLatencyView mTouchView; }
tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyView.java 0 → 100644 +219 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.prefabulated.touchlatency; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.os.Trace; import android.util.AttributeSet; import android.util.Log; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import java.math.RoundingMode; import java.text.DecimalFormat; class TouchLatencyView extends View implements View.OnTouchListener { private static final String LOG_TAG = "TouchLatency"; private static final int BACKGROUND_COLOR = 0xFF400080; private static final int INNER_RADIUS = 70; private static final int BALL_DIAMETER = 200; private static final int SEC_TO_NANOS = 1000000000; private static final float FPS_UPDATE_THRESHOLD = 20; private static final long BALL_VELOCITY = 420; public TouchLatencyView(Context context, AttributeSet attrs) { super(context, attrs); Trace.beginSection("TouchLatencyView constructor"); setOnTouchListener(this); setWillNotDraw(false); mBluePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mBluePaint.setColor(0xFF0000FF); mBluePaint.setStyle(Paint.Style.FILL); mGreenPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mGreenPaint.setColor(0xFF00FF00); mGreenPaint.setStyle(Paint.Style.FILL); mYellowPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mYellowPaint.setColor(0xFFFFFF00); mYellowPaint.setStyle(Paint.Style.FILL); mRedPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mRedPaint.setColor(0xFFFF0000); mRedPaint.setStyle(Paint.Style.FILL); mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mTextPaint.setColor(0xFFFFFFFF); mTextPaint.setTextSize(100); mTextPaint.setTextAlign(Paint.Align.RIGHT); mTouching = false; mLastDrawNano = 0; mFps = 0; mLastFpsUpdate = 0; mFrameCount = 0; mDf = new DecimalFormat("fps: #.##"); mDf.setRoundingMode(RoundingMode.HALF_UP); Trace.endSection(); } @Override public boolean onTouch(View view, MotionEvent event) { Trace.beginSection("TouchLatencyView onTouch"); int action = event.getActionMasked(); if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) { mTouching = true; invalidate(); mTouchX = event.getX(); mTouchY = event.getY(); } else if (action == MotionEvent.ACTION_UP) { mTouching = false; invalidate(); } Trace.endSection(); return true; } private void drawTouch(Canvas canvas) { Trace.beginSection("TouchLatencyView drawTouch"); try { if (!mTouching) { Log.d(LOG_TAG, "Filling background"); canvas.drawColor(BACKGROUND_COLOR); return; } float deltaX = (mTouchX - mLastDrawnX); float deltaY = (mTouchY - mLastDrawnY); float scaleFactor = (float) Math.sqrt(deltaX * deltaX + deltaY * deltaY) * 1.5f; mLastDrawnX = mTouchX; mLastDrawnY = mTouchY; canvas.drawColor(BACKGROUND_COLOR); canvas.drawCircle(mTouchX, mTouchY, INNER_RADIUS + 3 * scaleFactor, mRedPaint); canvas.drawCircle(mTouchX, mTouchY, INNER_RADIUS + 2 * scaleFactor, mYellowPaint); canvas.drawCircle(mTouchX, mTouchY, INNER_RADIUS + scaleFactor, mGreenPaint); canvas.drawCircle(mTouchX, mTouchY, INNER_RADIUS, mBluePaint); } finally { Trace.endSection(); } } private Paint getBallColor() { if (mFps > 75) { return mGreenPaint; } else if (mFps > 45) { return mYellowPaint; } else return mRedPaint; } private void drawBall(Canvas canvas) { Trace.beginSection("TouchLatencyView drawBall"); int width = canvas.getWidth(); int height = canvas.getHeight(); float fps = 0f; long t = System.nanoTime(); long tDiff = t - mLastDrawNano; mLastDrawNano = t; mFrameCount++; if (tDiff < SEC_TO_NANOS) { fps = 1f * SEC_TO_NANOS / tDiff; } long fDiff = t - mLastFpsUpdate; if (Math.abs(mFps - fps) > FPS_UPDATE_THRESHOLD) { mFps = fps; mLastFpsUpdate = t; mFrameCount = 0; } else if (fDiff > SEC_TO_NANOS) { mFps = 1f * mFrameCount * SEC_TO_NANOS / fDiff; mLastFpsUpdate = t; mFrameCount = 0; } final long pos = t * BALL_VELOCITY / SEC_TO_NANOS; final long xMax = width - BALL_DIAMETER; final long yMax = height - BALL_DIAMETER; long xOffset = pos % xMax; long yOffset = pos % yMax; float left, right, top, bottom; if (((pos / xMax) & 1) == 0) { left = xMax - xOffset; } else { left = xOffset; } right = left + BALL_DIAMETER; if (((pos / yMax) & 1) == 0) { top = yMax - yOffset; } else { top = yOffset; } bottom = top + BALL_DIAMETER; // Draw the ball canvas.drawColor(BACKGROUND_COLOR); canvas.drawOval(left, top, right, bottom, getBallColor()); canvas.drawText(mDf.format(mFps), width, 100, mTextPaint); invalidate(); Trace.endSection(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Trace.beginSection("TouchLatencyView onDraw"); if (mMode == 0) { drawTouch(canvas); } else { drawBall(canvas); } Trace.endSection(); } public void changeMode(MenuItem item) { Trace.beginSection("TouchLatencyView changeMode"); final int NUM_MODES = 2; final String modes[] = {"Touch", "Ball"}; mMode = (mMode + 1) % NUM_MODES; invalidate(); item.setTitle(modes[mMode]); Trace.endSection(); } private final Paint mBluePaint, mGreenPaint, mYellowPaint, mRedPaint, mTextPaint; private int mMode; private boolean mTouching; private float mTouchX, mTouchY; private float mLastDrawnX, mLastDrawnY; private long mLastDrawNano, mLastFpsUpdate, mFrameCount; private float mFps; private DecimalFormat mDf; }