Merge changes from topic "accessibility_floating_menu_widget" into sc-dev
* changes: Add new floating action menu for Accessibility targets. (5/n) Add new floating action menu for Accessibility targets. (4/n) Add new floating action menu for Accessibility targets. (3/n) Add new floating action menu for Accessibility targets. (2/n) Add new floating action menu for Accessibility targets. (1/n)
This commit is contained in:
committed by
Android (Google) Code Review
commit
62d3178166
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2021 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.
|
||||
-->
|
||||
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners
|
||||
android:bottomLeftRadius="@dimen/accessibility_floating_menu_small_single_radius"
|
||||
android:bottomRightRadius="0dp"
|
||||
android:topLeftRadius="@dimen/accessibility_floating_menu_small_single_radius"
|
||||
android:topRightRadius="0dp"/>
|
||||
<solid
|
||||
android:color="@color/accessibility_floating_menu_background"/>
|
||||
</shape>
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2021 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.
|
||||
-->
|
||||
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="@dimen/accessibility_floating_menu_padding"
|
||||
android:paddingEnd="@dimen/accessibility_floating_menu_padding"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
|
||||
<View
|
||||
android:id="@+id/icon_view"
|
||||
android:layout_width="@dimen/accessibility_floating_menu_small_width_height"
|
||||
android:layout_height="@dimen/accessibility_floating_menu_small_width_height"/>
|
||||
|
||||
<View
|
||||
android:id="@+id/transparent_divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/accessibility_floating_menu_padding"/>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -102,4 +102,6 @@
|
||||
<color name="privacy_circle_camera">#81C995</color> <!-- g300 -->
|
||||
<color name="privacy_circle_microphone_location">#FCAD70</color> <!--o300 -->
|
||||
|
||||
<!-- Accessibility floating menu -->
|
||||
<color name="accessibility_floating_menu_background">#B3000000</color> <!-- 70% -->
|
||||
</resources>
|
||||
|
||||
@@ -274,4 +274,8 @@
|
||||
<!-- TODO(b/178093014) Colors for privacy dialog. These should be changed to the new palette -->
|
||||
<color name="privacy_circle_camera">#1E8E3E</color> <!-- g600 -->
|
||||
<color name="privacy_circle_microphone_location">#E8710A</color> <!--o600 -->
|
||||
|
||||
<!-- Accessibility floating menu -->
|
||||
<color name="accessibility_floating_menu_background">#CCFFFFFF</color> <!-- 80% -->
|
||||
<color name="accessibility_floating_menu_stroke_dark">#26FFFFFF</color> <!-- 15% -->
|
||||
</resources>
|
||||
|
||||
@@ -1356,6 +1356,19 @@
|
||||
<dimen name="people_space_image_radius">20dp</dimen>
|
||||
<dimen name="people_space_widget_background_padding">6dp</dimen>
|
||||
|
||||
<!-- Accessibility floating menu -->
|
||||
<dimen name="accessibility_floating_menu_elevation">5dp</dimen>
|
||||
<dimen name="accessibility_floating_menu_stroke_width">1dp</dimen>
|
||||
<dimen name="accessibility_floating_menu_stroke_inset">-2dp</dimen>
|
||||
<dimen name="accessibility_floating_menu_margin">16dp</dimen>
|
||||
<dimen name="accessibility_floating_menu_padding">6dp</dimen>
|
||||
<dimen name="accessibility_floating_menu_small_width_height">36dp</dimen>
|
||||
<dimen name="accessibility_floating_menu_small_single_radius">25dp</dimen>
|
||||
<dimen name="accessibility_floating_menu_small_multiple_radius">20dp</dimen>
|
||||
<dimen name="accessibility_floating_menu_large_width_height">56dp</dimen>
|
||||
<dimen name="accessibility_floating_menu_large_single_radius">33dp</dimen>
|
||||
<dimen name="accessibility_floating_menu_large_multiple_radius">35dp</dimen>
|
||||
|
||||
<dimen name="rounded_slider_height">44dp</dimen>
|
||||
<!-- rounded_slider_height / 2 -->
|
||||
<dimen name="rounded_slider_corner_radius">22dp</dimen>
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.android.systemui.accessibility.floatingmenu;
|
||||
|
||||
import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED;
|
||||
import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_ICON_TYPE;
|
||||
import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_OPACITY;
|
||||
import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE;
|
||||
import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON;
|
||||
|
||||
import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getTargets;
|
||||
import static com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuView.ShapeType;
|
||||
import static com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuView.SizeType;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.ContentObserver;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.UserHandle;
|
||||
import android.provider.Settings;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
/**
|
||||
* Contains logic for an accessibility floating menu view.
|
||||
*/
|
||||
public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu {
|
||||
private static final int DEFAULT_FADE_EFFECT_ENABLED = 1;
|
||||
private static final float DEFAULT_OPACITY_VALUE = 0.55f;
|
||||
private final Context mContext;
|
||||
private final AccessibilityFloatingMenuView mMenuView;
|
||||
private final Handler mHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
private final ContentObserver mContentObserver =
|
||||
new ContentObserver(mHandler) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
mMenuView.onTargetsChanged(getTargets(mContext, ACCESSIBILITY_BUTTON));
|
||||
}
|
||||
};
|
||||
|
||||
private final ContentObserver mSizeContentObserver =
|
||||
new ContentObserver(mHandler) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
mMenuView.setSizeType(getSizeType(mContext));
|
||||
}
|
||||
};
|
||||
|
||||
private final ContentObserver mFadeOutContentObserver =
|
||||
new ContentObserver(mHandler) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
mMenuView.updateOpacityWith(isFadeEffectEnabled(mContext),
|
||||
getOpacityValue(mContext));
|
||||
}
|
||||
};
|
||||
|
||||
public AccessibilityFloatingMenu(Context context) {
|
||||
mContext = context;
|
||||
mMenuView = new AccessibilityFloatingMenuView(context);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
AccessibilityFloatingMenu(Context context, AccessibilityFloatingMenuView menuView) {
|
||||
mContext = context;
|
||||
mMenuView = menuView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isShowing() {
|
||||
return mMenuView.isShowing();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show() {
|
||||
if (isShowing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
mMenuView.show();
|
||||
mMenuView.onTargetsChanged(getTargets(mContext, ACCESSIBILITY_BUTTON));
|
||||
mMenuView.updateOpacityWith(isFadeEffectEnabled(mContext),
|
||||
getOpacityValue(mContext));
|
||||
mMenuView.setSizeType(getSizeType(mContext));
|
||||
mMenuView.setShapeType(getShapeType(mContext));
|
||||
|
||||
registerContentObservers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hide() {
|
||||
if (!isShowing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
mMenuView.hide();
|
||||
|
||||
unregisterContentObservers();
|
||||
}
|
||||
|
||||
private static boolean isFadeEffectEnabled(Context context) {
|
||||
return Settings.Secure.getInt(
|
||||
context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED,
|
||||
DEFAULT_FADE_EFFECT_ENABLED) == /* enable */ 1;
|
||||
}
|
||||
|
||||
private static float getOpacityValue(Context context) {
|
||||
return Settings.Secure.getFloat(
|
||||
context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_OPACITY,
|
||||
DEFAULT_OPACITY_VALUE);
|
||||
}
|
||||
|
||||
private static int getSizeType(Context context) {
|
||||
return Settings.Secure.getInt(
|
||||
context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_SIZE, SizeType.SMALL);
|
||||
}
|
||||
|
||||
private static int getShapeType(Context context) {
|
||||
return Settings.Secure.getInt(
|
||||
context.getContentResolver(), ACCESSIBILITY_FLOATING_MENU_ICON_TYPE,
|
||||
ShapeType.OVAL);
|
||||
}
|
||||
|
||||
private void registerContentObservers() {
|
||||
mContext.getContentResolver().registerContentObserver(
|
||||
Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS),
|
||||
/* notifyForDescendants */ false, mContentObserver,
|
||||
UserHandle.USER_CURRENT);
|
||||
mContext.getContentResolver().registerContentObserver(
|
||||
Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE),
|
||||
/* notifyForDescendants */ false, mSizeContentObserver,
|
||||
UserHandle.USER_CURRENT);
|
||||
mContext.getContentResolver().registerContentObserver(
|
||||
Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED),
|
||||
/* notifyForDescendants */ false, mFadeOutContentObserver,
|
||||
UserHandle.USER_CURRENT);
|
||||
mContext.getContentResolver().registerContentObserver(
|
||||
Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_OPACITY),
|
||||
/* notifyForDescendants */ false, mFadeOutContentObserver,
|
||||
UserHandle.USER_CURRENT);
|
||||
}
|
||||
|
||||
private void unregisterContentObservers() {
|
||||
mContext.getContentResolver().unregisterContentObserver(mContentObserver);
|
||||
mContext.getContentResolver().unregisterContentObserver(mSizeContentObserver);
|
||||
mContext.getContentResolver().unregisterContentObserver(mFadeOutContentObserver);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,690 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.android.systemui.accessibility.floatingmenu;
|
||||
|
||||
import static android.util.MathUtils.constrain;
|
||||
import static android.util.MathUtils.sq;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.IntDef;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.Gravity;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.OvershootInterpolator;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.DimenRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.android.internal.accessibility.dialog.AccessibilityTarget;
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.systemui.R;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Accessibility floating menu is used for the actions of accessibility features, it's also the
|
||||
* action set.
|
||||
*
|
||||
* <p>The number of items would depend on strings key
|
||||
* {@link android.provider.Settings.Secure#ACCESSIBILITY_BUTTON_TARGETS}.
|
||||
*/
|
||||
public class AccessibilityFloatingMenuView extends FrameLayout
|
||||
implements RecyclerView.OnItemTouchListener {
|
||||
private static final int INDEX_MENU_ITEM = 0;
|
||||
private static final int FADE_OUT_DURATION_MS = 1000;
|
||||
private static final int FADE_EFFECT_DURATION_MS = 3000;
|
||||
private static final int SNAP_TO_LOCATION_DURATION_MS = 150;
|
||||
private static final int MIN_WINDOW_X = 0;
|
||||
private static final int MIN_WINDOW_Y = 0;
|
||||
private static final float LOCATION_Y_PERCENTAGE = 0.8f;
|
||||
|
||||
private boolean mIsFadeEffectEnabled;
|
||||
private boolean mIsShowing;
|
||||
private boolean mIsDownInEnlargedTouchArea;
|
||||
private boolean mIsDragging = false;
|
||||
@Alignment
|
||||
private int mAlignment = Alignment.RIGHT;
|
||||
@SizeType
|
||||
private int mSizeType = SizeType.SMALL;
|
||||
@VisibleForTesting
|
||||
@ShapeType
|
||||
int mShapeType = ShapeType.OVAL;
|
||||
private int mTemporaryShapeType;
|
||||
@RadiusType
|
||||
private int mRadiusType = RadiusType.LEFT_HALF_OVAL;
|
||||
private int mMargin;
|
||||
private int mPadding;
|
||||
private int mScreenHeight;
|
||||
private int mScreenWidth;
|
||||
private int mIconWidth;
|
||||
private int mIconHeight;
|
||||
private int mInset;
|
||||
private int mDownX;
|
||||
private int mDownY;
|
||||
private int mRelativeToPointerDownX;
|
||||
private int mRelativeToPointerDownY;
|
||||
private float mRadius;
|
||||
private float mPercentageY = LOCATION_Y_PERCENTAGE;
|
||||
private float mSquareScaledTouchSlop;
|
||||
private final RecyclerView mListView;
|
||||
private final AccessibilityTargetAdapter mAdapter;
|
||||
private float mFadeOutValue;
|
||||
private final ValueAnimator mFadeOutAnimator;
|
||||
@VisibleForTesting
|
||||
final ValueAnimator mDragAnimator;
|
||||
private final Handler mUiHandler;
|
||||
@VisibleForTesting
|
||||
final WindowManager.LayoutParams mCurrentLayoutParams;
|
||||
private final WindowManager mWindowManager;
|
||||
private final List<AccessibilityTarget> mTargets = new ArrayList<>();
|
||||
|
||||
@IntDef({
|
||||
SizeType.SMALL,
|
||||
SizeType.LARGE
|
||||
})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@interface SizeType {
|
||||
int SMALL = 0;
|
||||
int LARGE = 1;
|
||||
}
|
||||
|
||||
@IntDef({
|
||||
ShapeType.OVAL,
|
||||
ShapeType.HALF_OVAL
|
||||
})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@interface ShapeType {
|
||||
int OVAL = 0;
|
||||
int HALF_OVAL = 1;
|
||||
}
|
||||
|
||||
@IntDef({
|
||||
RadiusType.LEFT_HALF_OVAL,
|
||||
RadiusType.OVAL,
|
||||
RadiusType.RIGHT_HALF_OVAL
|
||||
})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@interface RadiusType {
|
||||
int LEFT_HALF_OVAL = 0;
|
||||
int OVAL = 1;
|
||||
int RIGHT_HALF_OVAL = 2;
|
||||
}
|
||||
|
||||
@IntDef({
|
||||
Alignment.LEFT,
|
||||
Alignment.RIGHT
|
||||
})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@interface Alignment {
|
||||
int LEFT = 0;
|
||||
int RIGHT = 1;
|
||||
}
|
||||
|
||||
public AccessibilityFloatingMenuView(Context context) {
|
||||
this(context, new RecyclerView(context));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
AccessibilityFloatingMenuView(Context context,
|
||||
RecyclerView listView) {
|
||||
super(context);
|
||||
|
||||
mListView = listView;
|
||||
mWindowManager = context.getSystemService(WindowManager.class);
|
||||
mCurrentLayoutParams = createDefaultLayoutParams();
|
||||
mAdapter = new AccessibilityTargetAdapter(mTargets);
|
||||
mUiHandler = createUiHandler();
|
||||
|
||||
mFadeOutAnimator = ValueAnimator.ofFloat(1.0f, mFadeOutValue);
|
||||
mFadeOutAnimator.setDuration(FADE_OUT_DURATION_MS);
|
||||
mFadeOutAnimator.addUpdateListener(
|
||||
(animation) -> setAlpha((float) animation.getAnimatedValue()));
|
||||
|
||||
mDragAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
|
||||
mDragAnimator.setDuration(SNAP_TO_LOCATION_DURATION_MS);
|
||||
mDragAnimator.setInterpolator(new OvershootInterpolator());
|
||||
mDragAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
mAlignment = calculateCurrentAlignment();
|
||||
mPercentageY = calculateCurrentPercentageY();
|
||||
|
||||
updateLocationWith(mAlignment, mPercentageY);
|
||||
updateMarginsWith(mAlignment);
|
||||
|
||||
updateInsetWith(getResources().getConfiguration().uiMode, mAlignment);
|
||||
|
||||
mRadiusType = (mAlignment == Alignment.RIGHT)
|
||||
? RadiusType.LEFT_HALF_OVAL
|
||||
: RadiusType.RIGHT_HALF_OVAL;
|
||||
updateRadiusWith(mSizeType, mRadiusType, mTargets.size());
|
||||
|
||||
fadeOut();
|
||||
}
|
||||
});
|
||||
|
||||
updateDimensions();
|
||||
initListView();
|
||||
updateStrokeWith(getResources().getConfiguration().uiMode, mAlignment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
|
||||
@NonNull MotionEvent event) {
|
||||
final int currentRawX = (int) event.getRawX();
|
||||
final int currentRawY = (int) event.getRawY();
|
||||
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
fadeIn();
|
||||
|
||||
mDownX = currentRawX;
|
||||
mDownY = currentRawY;
|
||||
mRelativeToPointerDownX = mCurrentLayoutParams.x - mDownX;
|
||||
mRelativeToPointerDownY = mCurrentLayoutParams.y - mDownY;
|
||||
mListView.animate().translationX(0);
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (mIsDragging
|
||||
|| hasExceededTouchSlop(mDownX, mDownY, currentRawX, currentRawY)) {
|
||||
if (!mIsDragging) {
|
||||
mIsDragging = true;
|
||||
setRadius(mRadius, RadiusType.OVAL);
|
||||
setInset(0, 0);
|
||||
}
|
||||
|
||||
mTemporaryShapeType =
|
||||
isMovingTowardsScreenEdge(mAlignment, currentRawX, mDownX)
|
||||
? ShapeType.HALF_OVAL
|
||||
: ShapeType.OVAL;
|
||||
final int newWindowX = currentRawX + mRelativeToPointerDownX;
|
||||
final int newWindowY = currentRawY + mRelativeToPointerDownY;
|
||||
mCurrentLayoutParams.x = constrain(newWindowX, MIN_WINDOW_X, getMaxWindowX());
|
||||
mCurrentLayoutParams.y = constrain(newWindowY, MIN_WINDOW_Y, getMaxWindowY());
|
||||
mWindowManager.updateViewLayout(this, mCurrentLayoutParams);
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
if (mIsDragging) {
|
||||
mIsDragging = false;
|
||||
|
||||
final int maxX = getMaxWindowX();
|
||||
final int endX = mCurrentLayoutParams.x > ((MIN_WINDOW_X + maxX) / 2)
|
||||
? maxX : MIN_WINDOW_X;
|
||||
final int endY = mCurrentLayoutParams.y;
|
||||
snapToLocation(endX, endY);
|
||||
|
||||
setShapeType(mTemporaryShapeType);
|
||||
|
||||
// Avoid triggering the listener of the item.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Must switch the oval shape type before tapping the corresponding item in the
|
||||
// list view, otherwise it can't work on it.
|
||||
if (mShapeType == ShapeType.HALF_OVAL) {
|
||||
setShapeType(ShapeType.OVAL);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fadeOut();
|
||||
break;
|
||||
default: // Do nothing
|
||||
}
|
||||
|
||||
// not consume all the events here because keeping the scroll behavior of list view.
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {
|
||||
// Do Nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestDisallowInterceptTouchEvent(boolean b) {
|
||||
// Do Nothing
|
||||
}
|
||||
|
||||
void show() {
|
||||
if (isShowing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
mIsShowing = true;
|
||||
mWindowManager.addView(this, mCurrentLayoutParams);
|
||||
setSystemGestureExclusion();
|
||||
}
|
||||
|
||||
void hide() {
|
||||
if (!isShowing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
mIsShowing = false;
|
||||
mWindowManager.removeView(this);
|
||||
setSystemGestureExclusion();
|
||||
}
|
||||
|
||||
boolean isShowing() {
|
||||
return mIsShowing;
|
||||
}
|
||||
|
||||
void onTargetsChanged(List<AccessibilityTarget> newTargets) {
|
||||
fadeIn();
|
||||
|
||||
mTargets.clear();
|
||||
mTargets.addAll(newTargets);
|
||||
mAdapter.notifyDataSetChanged();
|
||||
|
||||
updateRadiusWith(mSizeType, mRadiusType, mTargets.size());
|
||||
setSystemGestureExclusion();
|
||||
|
||||
fadeOut();
|
||||
}
|
||||
|
||||
void setSizeType(@SizeType int newSizeType) {
|
||||
fadeIn();
|
||||
|
||||
mSizeType = newSizeType;
|
||||
|
||||
updateIconSizeWith(newSizeType);
|
||||
updateRadiusWith(newSizeType, mRadiusType, mTargets.size());
|
||||
|
||||
// When the icon sized changed, the menu size and location will be impacted.
|
||||
updateLocationWith(mAlignment, mPercentageY);
|
||||
setSystemGestureExclusion();
|
||||
|
||||
fadeOut();
|
||||
}
|
||||
|
||||
void setShapeType(@ShapeType int newShapeType) {
|
||||
fadeIn();
|
||||
|
||||
mShapeType = newShapeType;
|
||||
|
||||
updateOffsetWith(newShapeType, mAlignment);
|
||||
|
||||
setOnTouchListener(
|
||||
newShapeType == ShapeType.OVAL
|
||||
? null
|
||||
: (view, event) -> onTouched(event));
|
||||
|
||||
fadeOut();
|
||||
}
|
||||
|
||||
void updateOpacityWith(boolean isFadeEffectEnabled, float newOpacityValue) {
|
||||
mIsFadeEffectEnabled = isFadeEffectEnabled;
|
||||
mFadeOutValue = newOpacityValue;
|
||||
|
||||
mFadeOutAnimator.cancel();
|
||||
mFadeOutAnimator.setFloatValues(1.0f, mFadeOutValue);
|
||||
setAlpha(mIsFadeEffectEnabled ? mFadeOutValue : /* completely opaque */ 1.0f);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void fadeIn() {
|
||||
if (!mIsFadeEffectEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
mFadeOutAnimator.cancel();
|
||||
mUiHandler.removeCallbacksAndMessages(null);
|
||||
mUiHandler.post(() -> setAlpha(/* completely opaque */ 1.0f));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void fadeOut() {
|
||||
if (!mIsFadeEffectEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
mUiHandler.postDelayed(() -> mFadeOutAnimator.start(), FADE_EFFECT_DURATION_MS);
|
||||
}
|
||||
|
||||
private boolean onTouched(MotionEvent event) {
|
||||
final int action = event.getAction();
|
||||
final int currentX = (int) event.getX();
|
||||
final int currentY = (int) event.getY();
|
||||
|
||||
final int menuHalfWidth = getLayoutWidth() / 2;
|
||||
final Rect touchDelegateBounds =
|
||||
new Rect(mMargin, mMargin, mMargin + menuHalfWidth, mMargin + getLayoutHeight());
|
||||
if (action == MotionEvent.ACTION_DOWN
|
||||
&& touchDelegateBounds.contains(currentX, currentY)) {
|
||||
mIsDownInEnlargedTouchArea = true;
|
||||
}
|
||||
|
||||
if (!mIsDownInEnlargedTouchArea) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (action == MotionEvent.ACTION_UP
|
||||
|| action == MotionEvent.ACTION_CANCEL) {
|
||||
mIsDownInEnlargedTouchArea = false;
|
||||
}
|
||||
|
||||
// In order to correspond to the correct item of list view.
|
||||
event.setLocation(currentX - mMargin, currentY - mMargin);
|
||||
return mListView.dispatchTouchEvent(event);
|
||||
}
|
||||
|
||||
private boolean isMovingTowardsScreenEdge(@Alignment int side, int currentRawX, int downX) {
|
||||
return (side == Alignment.RIGHT && currentRawX > downX)
|
||||
|| (side == Alignment.LEFT && downX > currentRawX);
|
||||
}
|
||||
|
||||
private boolean hasExceededTouchSlop(int startX, int startY, int endX, int endY) {
|
||||
return (sq(endX - startX) + sq(endY - startY)) > mSquareScaledTouchSlop;
|
||||
}
|
||||
|
||||
private void setRadius(float radius, @RadiusType int type) {
|
||||
getMenuGradientDrawable().setCornerRadii(createRadii(radius, type));
|
||||
}
|
||||
|
||||
private float[] createRadii(float radius, @RadiusType int type) {
|
||||
if (type == RadiusType.LEFT_HALF_OVAL) {
|
||||
return new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius};
|
||||
}
|
||||
|
||||
if (type == RadiusType.RIGHT_HALF_OVAL) {
|
||||
return new float[]{0.0f, 0.0f, radius, radius, radius, radius, 0.0f, 0.0f};
|
||||
}
|
||||
|
||||
return new float[]{radius, radius, radius, radius, radius, radius, radius, radius};
|
||||
}
|
||||
|
||||
private Handler createUiHandler() {
|
||||
final Looper looper = Looper.myLooper();
|
||||
if (looper == null) {
|
||||
throw new IllegalArgumentException("looper must not be null");
|
||||
}
|
||||
return new Handler(looper);
|
||||
}
|
||||
|
||||
private void updateDimensions() {
|
||||
final Resources res = getResources();
|
||||
final DisplayMetrics dm = res.getDisplayMetrics();
|
||||
mScreenWidth = dm.widthPixels;
|
||||
mScreenHeight = dm.heightPixels;
|
||||
mMargin =
|
||||
res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_margin);
|
||||
mPadding =
|
||||
res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_padding);
|
||||
mInset =
|
||||
res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_stroke_inset);
|
||||
|
||||
mSquareScaledTouchSlop =
|
||||
sq(ViewConfiguration.get(getContext()).getScaledTouchSlop());
|
||||
}
|
||||
|
||||
private void updateIconSizeWith(@SizeType int sizeType) {
|
||||
final Resources res = getResources();
|
||||
final int iconResId =
|
||||
sizeType == SizeType.SMALL
|
||||
? R.dimen.accessibility_floating_menu_small_width_height
|
||||
: R.dimen.accessibility_floating_menu_large_width_height;
|
||||
mIconWidth = res.getDimensionPixelSize(iconResId);
|
||||
mIconHeight = mIconWidth;
|
||||
|
||||
mAdapter.setIconWidthHeight(mIconWidth);
|
||||
mAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void initListView() {
|
||||
final Drawable background =
|
||||
getContext().getDrawable(R.drawable.accessibility_floating_menu_background);
|
||||
final LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
|
||||
final LayoutParams layoutParams =
|
||||
new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
mListView.setLayoutParams(layoutParams);
|
||||
final InstantInsetLayerDrawable layerDrawable =
|
||||
new InstantInsetLayerDrawable(new Drawable[]{background});
|
||||
mListView.setBackground(layerDrawable);
|
||||
mListView.setAdapter(mAdapter);
|
||||
mListView.setLayoutManager(layoutManager);
|
||||
mListView.addOnItemTouchListener(this);
|
||||
mListView.animate().setInterpolator(new OvershootInterpolator());
|
||||
updateListView();
|
||||
|
||||
addView(mListView);
|
||||
}
|
||||
|
||||
private void updateListView() {
|
||||
final int elevation =
|
||||
getResources().getDimensionPixelSize(R.dimen.accessibility_floating_menu_elevation);
|
||||
mListView.setElevation(elevation);
|
||||
|
||||
updateMarginsWith(mAlignment);
|
||||
}
|
||||
|
||||
private WindowManager.LayoutParams createDefaultLayoutParams() {
|
||||
final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
PixelFormat.TRANSLUCENT);
|
||||
params.windowAnimations = android.R.style.Animation_Translucent;
|
||||
params.gravity = Gravity.START | Gravity.TOP;
|
||||
params.x = getMaxWindowX();
|
||||
params.y = (int) (getMaxWindowY() * mPercentageY);
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
updateDimensions();
|
||||
updateListView();
|
||||
updateIconSizeWith(mSizeType);
|
||||
updateColor();
|
||||
updateStrokeWith(newConfig.uiMode, mAlignment);
|
||||
updateLocationWith(mAlignment, mPercentageY);
|
||||
}
|
||||
|
||||
private void snapToLocation(int endX, int endY) {
|
||||
mDragAnimator.cancel();
|
||||
mDragAnimator.removeAllUpdateListeners();
|
||||
mDragAnimator.addUpdateListener(anim -> onDragAnimationUpdate(anim, endX, endY));
|
||||
mDragAnimator.start();
|
||||
}
|
||||
|
||||
private void onDragAnimationUpdate(ValueAnimator animator, int endX, int endY) {
|
||||
float value = (float) animator.getAnimatedValue();
|
||||
final int newX = (int) (((1 - value) * mCurrentLayoutParams.x) + (value * endX));
|
||||
final int newY = (int) (((1 - value) * mCurrentLayoutParams.y) + (value * endY));
|
||||
|
||||
mCurrentLayoutParams.x = newX;
|
||||
mCurrentLayoutParams.y = newY;
|
||||
mWindowManager.updateViewLayout(this, mCurrentLayoutParams);
|
||||
}
|
||||
|
||||
private int getMaxWindowX() {
|
||||
return mScreenWidth - mMargin - getLayoutWidth();
|
||||
}
|
||||
|
||||
private int getMaxWindowY() {
|
||||
return mScreenHeight - getWindowHeight();
|
||||
}
|
||||
|
||||
private InstantInsetLayerDrawable getMenuLayerDrawable() {
|
||||
return (InstantInsetLayerDrawable) mListView.getBackground();
|
||||
}
|
||||
|
||||
private GradientDrawable getMenuGradientDrawable() {
|
||||
return (GradientDrawable) getMenuLayerDrawable().getDrawable(INDEX_MENU_ITEM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the floating menu to be fixed at the side of the screen.
|
||||
*/
|
||||
private void updateLocationWith(@Alignment int side, float percentageCurrentY) {
|
||||
mCurrentLayoutParams.x = (side == Alignment.RIGHT) ? getMaxWindowX() : MIN_WINDOW_X;
|
||||
mCurrentLayoutParams.y = (int) (percentageCurrentY * getMaxWindowY());
|
||||
mWindowManager.updateViewLayout(this, mCurrentLayoutParams);
|
||||
}
|
||||
|
||||
private void updateOffsetWith(@ShapeType int shapeType, @Alignment int side) {
|
||||
final float halfWidth = getLayoutWidth() / 2.0f;
|
||||
final float offset = (shapeType == ShapeType.OVAL) ? 0 : halfWidth;
|
||||
mListView.animate().translationX(side == Alignment.RIGHT ? offset : -offset);
|
||||
}
|
||||
|
||||
private void updateMarginsWith(@Alignment int side) {
|
||||
final LayoutParams layoutParams = (LayoutParams) mListView.getLayoutParams();
|
||||
final int marginLeft = (side == Alignment.LEFT) ? 0 : mMargin;
|
||||
final int marginRight = (side == Alignment.RIGHT) ? 0 : mMargin;
|
||||
|
||||
if (marginLeft == layoutParams.leftMargin
|
||||
&& marginRight == layoutParams.rightMargin) {
|
||||
return;
|
||||
}
|
||||
|
||||
layoutParams.setMargins(marginLeft, mMargin, marginRight, mMargin);
|
||||
mListView.setLayoutParams(layoutParams);
|
||||
}
|
||||
|
||||
private void updateColor() {
|
||||
final int menuColorResId = R.color.accessibility_floating_menu_background;
|
||||
getMenuGradientDrawable().setColor(getResources().getColor(menuColorResId));
|
||||
}
|
||||
|
||||
private void updateStrokeWith(int uiMode, @Alignment int side) {
|
||||
updateInsetWith(uiMode, side);
|
||||
|
||||
final boolean isNightMode =
|
||||
(uiMode & Configuration.UI_MODE_NIGHT_MASK)
|
||||
== Configuration.UI_MODE_NIGHT_YES;
|
||||
final Resources res = getResources();
|
||||
final int width =
|
||||
res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_stroke_width);
|
||||
final int strokeWidth = isNightMode ? width : 0;
|
||||
final int strokeColor = res.getColor(R.color.accessibility_floating_menu_stroke_dark);
|
||||
getMenuGradientDrawable().setStroke(strokeWidth, strokeColor);
|
||||
}
|
||||
|
||||
private void updateRadiusWith(@SizeType int sizeType, @RadiusType int radiusType,
|
||||
int itemCount) {
|
||||
mRadius =
|
||||
getResources().getDimensionPixelSize(getRadiusResId(sizeType, itemCount));
|
||||
setRadius(mRadius, radiusType);
|
||||
}
|
||||
|
||||
private void updateInsetWith(int uiMode, @Alignment int side) {
|
||||
final boolean isNightMode =
|
||||
(uiMode & Configuration.UI_MODE_NIGHT_MASK)
|
||||
== Configuration.UI_MODE_NIGHT_YES;
|
||||
|
||||
final int layerInset = isNightMode ? mInset : 0;
|
||||
final int insetLeft = (side == Alignment.LEFT) ? layerInset : 0;
|
||||
final int insetRight = (side == Alignment.RIGHT) ? layerInset : 0;
|
||||
setInset(insetLeft, insetRight);
|
||||
}
|
||||
|
||||
private void setInset(int left, int right) {
|
||||
final LayerDrawable layerDrawable = getMenuLayerDrawable();
|
||||
if (layerDrawable.getLayerInsetLeft(INDEX_MENU_ITEM) == left
|
||||
&& layerDrawable.getLayerInsetRight(INDEX_MENU_ITEM) == right) {
|
||||
return;
|
||||
}
|
||||
|
||||
layerDrawable.setLayerInset(INDEX_MENU_ITEM, left, 0, right, 0);
|
||||
}
|
||||
|
||||
@Alignment
|
||||
private int calculateCurrentAlignment() {
|
||||
return mCurrentLayoutParams.x >= ((MIN_WINDOW_X + getMaxWindowX()) / 2)
|
||||
? Alignment.RIGHT
|
||||
: Alignment.LEFT;
|
||||
}
|
||||
|
||||
private float calculateCurrentPercentageY() {
|
||||
return mCurrentLayoutParams.y / (float) getMaxWindowY();
|
||||
}
|
||||
|
||||
private @DimenRes int getRadiusResId(@SizeType int sizeType, int itemCount) {
|
||||
return sizeType == SizeType.SMALL
|
||||
? getSmallSizeResIdWith(itemCount)
|
||||
: getLargeSizeResIdWith(itemCount);
|
||||
}
|
||||
|
||||
private int getSmallSizeResIdWith(int itemCount) {
|
||||
return itemCount > 1
|
||||
? R.dimen.accessibility_floating_menu_small_multiple_radius
|
||||
: R.dimen.accessibility_floating_menu_small_single_radius;
|
||||
}
|
||||
|
||||
private int getLargeSizeResIdWith(int itemCount) {
|
||||
return itemCount > 1
|
||||
? R.dimen.accessibility_floating_menu_large_multiple_radius
|
||||
: R.dimen.accessibility_floating_menu_large_single_radius;
|
||||
}
|
||||
|
||||
private int getLayoutWidth() {
|
||||
return mPadding * 2 + mIconWidth;
|
||||
}
|
||||
|
||||
private int getLayoutHeight() {
|
||||
return Math.min(mScreenHeight - mMargin * 2,
|
||||
(mPadding + mIconHeight) * mTargets.size() + mPadding);
|
||||
}
|
||||
|
||||
private int getWindowWidth() {
|
||||
return mMargin + getLayoutWidth();
|
||||
}
|
||||
|
||||
private int getWindowHeight() {
|
||||
return Math.min(mScreenHeight, mMargin * 2 + getLayoutHeight());
|
||||
}
|
||||
|
||||
private void setSystemGestureExclusion() {
|
||||
final Rect excludeZone =
|
||||
new Rect(0, 0, getWindowWidth(), getWindowHeight());
|
||||
post(() -> setSystemGestureExclusionRects(
|
||||
mIsShowing
|
||||
? Collections.singletonList(excludeZone)
|
||||
: Collections.emptyList()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.android.systemui.accessibility.floatingmenu;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
|
||||
import static com.android.systemui.accessibility.floatingmenu.AccessibilityTargetAdapter.ItemType.FIRST_ITEM;
|
||||
import static com.android.systemui.accessibility.floatingmenu.AccessibilityTargetAdapter.ItemType.LAST_ITEM;
|
||||
import static com.android.systemui.accessibility.floatingmenu.AccessibilityTargetAdapter.ItemType.REGULAR_ITEM;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.Adapter;
|
||||
|
||||
import com.android.internal.accessibility.dialog.AccessibilityTarget;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.accessibility.floatingmenu.AccessibilityTargetAdapter.ViewHolder;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An adapter which shows the set of accessibility targets that can be performed.
|
||||
*/
|
||||
public class AccessibilityTargetAdapter extends Adapter<ViewHolder> {
|
||||
private int mIconWidthHeight;
|
||||
private final List<AccessibilityTarget> mTargets;
|
||||
|
||||
@IntDef({
|
||||
FIRST_ITEM,
|
||||
REGULAR_ITEM,
|
||||
LAST_ITEM
|
||||
})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@interface ItemType {
|
||||
int FIRST_ITEM = 0;
|
||||
int REGULAR_ITEM = 1;
|
||||
int LAST_ITEM = 2;
|
||||
}
|
||||
|
||||
public AccessibilityTargetAdapter(List<AccessibilityTarget> targets) {
|
||||
mTargets = targets;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, @ItemType int itemType) {
|
||||
final View root = LayoutInflater.from(parent.getContext()).inflate(
|
||||
R.layout.accessibility_floating_menu_item, parent,
|
||||
/* attachToRoot= */ false);
|
||||
|
||||
if (itemType == FIRST_ITEM) {
|
||||
return new TopViewHolder(root);
|
||||
}
|
||||
|
||||
if (itemType == LAST_ITEM) {
|
||||
return new BottomViewHolder(root);
|
||||
}
|
||||
|
||||
return new ViewHolder(root);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
holder.mIconView.setBackground(mTargets.get(position).getIcon());
|
||||
holder.updateIconWidthHeight(mIconWidthHeight);
|
||||
holder.itemView.setOnClickListener((v) -> mTargets.get(position).onSelected());
|
||||
}
|
||||
|
||||
@ItemType
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (position == 0) {
|
||||
return FIRST_ITEM;
|
||||
}
|
||||
|
||||
if (position == (getItemCount() - 1)) {
|
||||
return LAST_ITEM;
|
||||
}
|
||||
|
||||
return REGULAR_ITEM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mTargets.size();
|
||||
}
|
||||
|
||||
public void setIconWidthHeight(int iconWidthHeight) {
|
||||
mIconWidthHeight = iconWidthHeight;
|
||||
}
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
final View mIconView;
|
||||
final View mDivider;
|
||||
|
||||
ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
mIconView = itemView.findViewById(R.id.icon_view);
|
||||
mDivider = itemView.findViewById(R.id.transparent_divider);
|
||||
}
|
||||
|
||||
void updateIconWidthHeight(int newValue) {
|
||||
final ViewGroup.LayoutParams layoutParams = mIconView.getLayoutParams();
|
||||
if (layoutParams.width == newValue) {
|
||||
return;
|
||||
}
|
||||
layoutParams.width = newValue;
|
||||
layoutParams.height = newValue;
|
||||
mIconView.setLayoutParams(layoutParams);
|
||||
}
|
||||
}
|
||||
|
||||
static class TopViewHolder extends ViewHolder {
|
||||
TopViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
final int padding = itemView.getPaddingStart();
|
||||
itemView.setPaddingRelative(padding, padding, padding, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static class BottomViewHolder extends ViewHolder {
|
||||
BottomViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
mDivider.setVisibility(GONE);
|
||||
final int padding = itemView.getPaddingStart();
|
||||
itemView.setPaddingRelative(padding, 0, padding, padding);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.android.systemui.accessibility.floatingmenu;
|
||||
|
||||
/**
|
||||
* Interface for managing the accessibility targets menu component.
|
||||
*/
|
||||
public interface IAccessibilityFloatingMenu {
|
||||
|
||||
/**
|
||||
* Checks if the menu was shown.
|
||||
*/
|
||||
boolean isShowing();
|
||||
|
||||
/**
|
||||
* Shows the accessibility targets menu.
|
||||
*/
|
||||
void show();
|
||||
|
||||
/**
|
||||
* Hides the accessibility targets menu.
|
||||
*/
|
||||
void hide();
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.android.systemui.accessibility.floatingmenu;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
|
||||
/**
|
||||
* A drawable that forces to update the bounds {@link #onBoundsChange(Rect)} immediately after
|
||||
* {@link #setLayerInset} dynamically.
|
||||
*/
|
||||
public class InstantInsetLayerDrawable extends LayerDrawable {
|
||||
public InstantInsetLayerDrawable(Drawable[] layers) {
|
||||
super(layers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLayerInset(int index, int l, int t, int r, int b) {
|
||||
super.setLayerInset(index, l, t, r, b);
|
||||
onBoundsChange(getBounds());
|
||||
}
|
||||
}
|
||||
@@ -23,11 +23,11 @@ import com.android.internal.annotations.GuardedBy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
class MotionEventHelper {
|
||||
public class MotionEventHelper {
|
||||
@GuardedBy("this")
|
||||
private final List<MotionEvent> mMotionEvents = new ArrayList<>();
|
||||
|
||||
void recycleEvents() {
|
||||
public void recycleEvents() {
|
||||
for (MotionEvent event:mMotionEvents) {
|
||||
event.recycle();
|
||||
}
|
||||
@@ -36,7 +36,7 @@ class MotionEventHelper {
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent obtainMotionEvent(long downTime, long eventTime, int action, float x,
|
||||
public MotionEvent obtainMotionEvent(long downTime, long eventTime, int action, float x,
|
||||
float y) {
|
||||
MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, x, y, 0);
|
||||
synchronized (this) {
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.android.systemui.accessibility.floatingmenu;
|
||||
|
||||
import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import android.content.Context;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.testing.TestableLooper;
|
||||
import android.view.accessibility.AccessibilityManager;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.internal.accessibility.dialog.AccessibilityTarget;
|
||||
import com.android.systemui.SysuiTestCase;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Tests for {@link AccessibilityFloatingMenu}. */
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
@TestableLooper.RunWithLooper
|
||||
public class AccessibilityFloatingMenuTest extends SysuiTestCase {
|
||||
|
||||
@Mock
|
||||
private AccessibilityManager mAccessibilityManager;
|
||||
|
||||
private AccessibilityFloatingMenuView mMenuView;
|
||||
private AccessibilityFloatingMenu mMenu;
|
||||
|
||||
@Before
|
||||
public void initMenu() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
final List<AccessibilityTarget> mTargets = new ArrayList<>();
|
||||
mTargets.add(mock(AccessibilityTarget.class));
|
||||
|
||||
final List<String> assignedTargets = new ArrayList<>();
|
||||
mContext.addMockSystemService(Context.ACCESSIBILITY_SERVICE, mAccessibilityManager);
|
||||
assignedTargets.add(MAGNIFICATION_CONTROLLER_NAME);
|
||||
doReturn(assignedTargets).when(mAccessibilityManager).getAccessibilityShortcutTargets(
|
||||
anyInt());
|
||||
|
||||
mMenuView = new AccessibilityFloatingMenuView(mContext);
|
||||
mMenu = new AccessibilityFloatingMenu(mContext, mMenuView);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void showMenuView_success() {
|
||||
mMenu.show();
|
||||
|
||||
assertThat(mMenuView.isShowing()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hideMenuView_success() {
|
||||
mMenu.show();
|
||||
mMenu.hide();
|
||||
|
||||
assertThat(mMenuView.isShowing()).isFalse();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
mMenu.hide();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.android.systemui.accessibility.floatingmenu;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyFloat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.testing.TestableLooper;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewPropertyAnimator;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.internal.accessibility.dialog.AccessibilityTarget;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.SysuiTestCase;
|
||||
import com.android.systemui.accessibility.MotionEventHelper;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InOrder;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Tests for {@link AccessibilityFloatingMenuView}. */
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
@TestableLooper.RunWithLooper
|
||||
public class AccessibilityFloatingMenuViewTest extends SysuiTestCase {
|
||||
private AccessibilityFloatingMenuView mMenuView;
|
||||
|
||||
@Mock
|
||||
private WindowManager mWindowManager;
|
||||
|
||||
@Mock
|
||||
private ViewPropertyAnimator mAnimator;
|
||||
|
||||
private MotionEvent mInterceptMotionEvent;
|
||||
|
||||
private RecyclerView mListView;
|
||||
|
||||
private int mMenuHalfWidth;
|
||||
private int mMenuHalfHeight;
|
||||
private int mScreenHalfWidth;
|
||||
private int mScreenHalfHeight;
|
||||
private int mMaxWindowX;
|
||||
|
||||
private final MotionEventHelper mMotionEventHelper = new MotionEventHelper();
|
||||
private final List<AccessibilityTarget> mTargets = new ArrayList<>();
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
final WindowManager wm = mContext.getSystemService(WindowManager.class);
|
||||
doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
|
||||
mWindowManager).getMaximumWindowMetrics();
|
||||
mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
|
||||
|
||||
mTargets.add(mock(AccessibilityTarget.class));
|
||||
mListView = new RecyclerView(mContext);
|
||||
mMenuView = new AccessibilityFloatingMenuView(mContext, mListView);
|
||||
|
||||
final Resources res = mContext.getResources();
|
||||
final int margin =
|
||||
res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_margin);
|
||||
final int padding =
|
||||
res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_padding);
|
||||
final int iconWidthHeight =
|
||||
res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_small_width_height);
|
||||
final int menuWidth = padding * 2 + iconWidthHeight;
|
||||
final int menuHeight = (padding + iconWidthHeight) * mTargets.size() + padding;
|
||||
final int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels;
|
||||
final int screenHeight = mContext.getResources().getDisplayMetrics().heightPixels;
|
||||
mMenuHalfWidth = menuWidth / 2;
|
||||
mMenuHalfHeight = menuHeight / 2;
|
||||
mScreenHalfWidth = screenWidth / 2;
|
||||
mScreenHalfHeight = screenHeight / 2;
|
||||
mMaxWindowX = screenWidth - margin - menuWidth;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initListView_success() {
|
||||
assertThat(mMenuView.getChildCount()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void showMenuView_success() {
|
||||
mMenuView.show();
|
||||
|
||||
assertThat(mMenuView.isShowing()).isTrue();
|
||||
verify(mWindowManager).addView(eq(mMenuView), any(WindowManager.LayoutParams.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void showMenuView_showTwice_addViewOnce() {
|
||||
mMenuView.show();
|
||||
mMenuView.show();
|
||||
|
||||
assertThat(mMenuView.isShowing()).isTrue();
|
||||
verify(mWindowManager, times(1)).addView(eq(mMenuView),
|
||||
any(WindowManager.LayoutParams.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hideMenuView_success() {
|
||||
mMenuView.show();
|
||||
mMenuView.hide();
|
||||
|
||||
assertThat(mMenuView.isShowing()).isFalse();
|
||||
verify(mWindowManager).removeView(eq(mMenuView));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hideMenuView_hideTwice_removeViewOnce() {
|
||||
mMenuView.show();
|
||||
mMenuView.hide();
|
||||
mMenuView.hide();
|
||||
|
||||
assertThat(mMenuView.isShowing()).isFalse();
|
||||
verify(mWindowManager, times(1)).removeView(eq(mMenuView));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateListViewRadius_singleTarget_matchResult() {
|
||||
final float radius =
|
||||
getContext().getResources().getDimensionPixelSize(
|
||||
R.dimen.accessibility_floating_menu_small_single_radius);
|
||||
final float[] expectedRadii =
|
||||
new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius};
|
||||
|
||||
mMenuView.onTargetsChanged(mTargets);
|
||||
final View view = mMenuView.getChildAt(0);
|
||||
final LayerDrawable layerDrawable = (LayerDrawable) view.getBackground();
|
||||
final GradientDrawable gradientDrawable =
|
||||
(GradientDrawable) layerDrawable.getDrawable(0);
|
||||
final float[] actualRadii = gradientDrawable.getCornerRadii();
|
||||
|
||||
assertThat(actualRadii).isEqualTo(expectedRadii);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setSizeType_largeSize_matchResult() {
|
||||
final int shapeType = 2;
|
||||
final float radius = getContext().getResources().getDimensionPixelSize(
|
||||
R.dimen.accessibility_floating_menu_large_single_radius);
|
||||
final float[] expectedRadii =
|
||||
new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius};
|
||||
final Drawable listViewBackground =
|
||||
mContext.getDrawable(R.drawable.accessibility_floating_menu_background);
|
||||
mListView = spy(new RecyclerView(mContext));
|
||||
mListView.setBackground(listViewBackground);
|
||||
|
||||
mMenuView = new AccessibilityFloatingMenuView(mContext, mListView);
|
||||
mMenuView.setSizeType(shapeType);
|
||||
final LayerDrawable layerDrawable =
|
||||
(LayerDrawable) mListView.getBackground();
|
||||
final GradientDrawable gradientDrawable =
|
||||
(GradientDrawable) layerDrawable.getDrawable(0);
|
||||
|
||||
assertThat(gradientDrawable.getCornerRadii()).isEqualTo(expectedRadii);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setShapeType_halfCircle_translationX() {
|
||||
final RecyclerView listView = spy(new RecyclerView(mContext));
|
||||
final AccessibilityFloatingMenuView menuView =
|
||||
new AccessibilityFloatingMenuView(mContext, listView);
|
||||
final int shapeType = 2;
|
||||
doReturn(mAnimator).when(listView).animate();
|
||||
|
||||
menuView.setShapeType(shapeType);
|
||||
|
||||
verify(mAnimator).translationX(anyFloat());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onTargetsChanged_fadeInOut() {
|
||||
final AccessibilityFloatingMenuView menuView = spy(mMenuView);
|
||||
final InOrder inOrderMenuView = inOrder(menuView);
|
||||
|
||||
menuView.onTargetsChanged(mTargets);
|
||||
|
||||
inOrderMenuView.verify(menuView).fadeIn();
|
||||
inOrderMenuView.verify(menuView).fadeOut();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setSizeType_fadeInOut() {
|
||||
final AccessibilityFloatingMenuView menuView = spy(mMenuView);
|
||||
final InOrder inOrderMenuView = inOrder(menuView);
|
||||
final int smallSize = 0;
|
||||
menuView.setSizeType(smallSize);
|
||||
|
||||
inOrderMenuView.verify(menuView).fadeIn();
|
||||
inOrderMenuView.verify(menuView).fadeOut();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tapOnAndDragMenu_interceptUpEvent() {
|
||||
final RecyclerView listView = new RecyclerView(mContext);
|
||||
final TestAccessibilityFloatingMenu menuView =
|
||||
new TestAccessibilityFloatingMenu(mContext, listView);
|
||||
|
||||
menuView.show();
|
||||
menuView.onTargetsChanged(mTargets);
|
||||
menuView.setSizeType(0);
|
||||
menuView.setShapeType(0);
|
||||
final int currentWindowX = mMenuView.mCurrentLayoutParams.x;
|
||||
final int currentWindowY = mMenuView.mCurrentLayoutParams.y;
|
||||
final MotionEvent downEvent =
|
||||
mMotionEventHelper.obtainMotionEvent(0, 1,
|
||||
MotionEvent.ACTION_DOWN,
|
||||
currentWindowX + /* offsetXToMenuCenterX */ mMenuHalfWidth,
|
||||
currentWindowY + /* offsetYToMenuCenterY */ mMenuHalfHeight);
|
||||
final MotionEvent moveEvent =
|
||||
mMotionEventHelper.obtainMotionEvent(2, 3,
|
||||
MotionEvent.ACTION_MOVE,
|
||||
/* screenCenterX */mScreenHalfWidth
|
||||
- /* offsetXToScreenLeftHalfRegion */ 10,
|
||||
/* screenCenterY */ mScreenHalfHeight);
|
||||
final MotionEvent upEvent =
|
||||
mMotionEventHelper.obtainMotionEvent(4, 5,
|
||||
MotionEvent.ACTION_UP,
|
||||
/* screenCenterX */ mScreenHalfWidth
|
||||
- /* offsetXToScreenLeftHalfRegion */ 10,
|
||||
/* screenCenterY */ mScreenHalfHeight);
|
||||
listView.dispatchTouchEvent(downEvent);
|
||||
listView.dispatchTouchEvent(moveEvent);
|
||||
listView.dispatchTouchEvent(upEvent);
|
||||
|
||||
assertThat(mInterceptMotionEvent.getAction()).isEqualTo(MotionEvent.ACTION_UP);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tapOnAndDragMenu_matchLocation() {
|
||||
mMenuView.show();
|
||||
mMenuView.onTargetsChanged(mTargets);
|
||||
mMenuView.setSizeType(0);
|
||||
mMenuView.setShapeType(0);
|
||||
final int currentWindowX = mMenuView.mCurrentLayoutParams.x;
|
||||
final int currentWindowY = mMenuView.mCurrentLayoutParams.y;
|
||||
final MotionEvent downEvent =
|
||||
mMotionEventHelper.obtainMotionEvent(0, 1,
|
||||
MotionEvent.ACTION_DOWN,
|
||||
currentWindowX + /* offsetXToMenuCenterX */ mMenuHalfWidth,
|
||||
currentWindowY + /* offsetYToMenuCenterY */ mMenuHalfHeight);
|
||||
final MotionEvent moveEvent =
|
||||
mMotionEventHelper.obtainMotionEvent(2, 3,
|
||||
MotionEvent.ACTION_MOVE,
|
||||
/* screenCenterX */mScreenHalfWidth
|
||||
+ /* offsetXToScreenRightHalfRegion */ 10,
|
||||
/* screenCenterY */ mScreenHalfHeight);
|
||||
final MotionEvent upEvent =
|
||||
mMotionEventHelper.obtainMotionEvent(4, 5,
|
||||
MotionEvent.ACTION_UP,
|
||||
/* screenCenterX */ mScreenHalfWidth
|
||||
+ /* offsetXToScreenRightHalfRegion */ 10,
|
||||
/* screenCenterY */ mScreenHalfHeight);
|
||||
mListView.dispatchTouchEvent(downEvent);
|
||||
mListView.dispatchTouchEvent(moveEvent);
|
||||
mListView.dispatchTouchEvent(upEvent);
|
||||
mMenuView.mDragAnimator.end();
|
||||
|
||||
assertThat(mMenuView.mCurrentLayoutParams.x).isEqualTo(mMaxWindowX);
|
||||
assertThat(mMenuView.mCurrentLayoutParams.y).isEqualTo(
|
||||
/* newWindowY = screenCenterY - offsetY */ mScreenHalfHeight - mMenuHalfHeight);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void tapOnAndDragMenuToScreenSide_transformShapeHalfOval() {
|
||||
mMenuView.show();
|
||||
mMenuView.onTargetsChanged(mTargets);
|
||||
mMenuView.setSizeType(0);
|
||||
mMenuView.setShapeType(/* oval */ 0);
|
||||
final int currentWindowX = mMenuView.mCurrentLayoutParams.x;
|
||||
final int currentWindowY = mMenuView.mCurrentLayoutParams.y;
|
||||
final MotionEvent downEvent =
|
||||
mMotionEventHelper.obtainMotionEvent(0, 1,
|
||||
MotionEvent.ACTION_DOWN,
|
||||
currentWindowX + /* offsetXToMenuCenterX */ mMenuHalfWidth,
|
||||
currentWindowY + /* offsetYToMenuCenterY */ mMenuHalfHeight);
|
||||
final MotionEvent moveEvent =
|
||||
mMotionEventHelper.obtainMotionEvent(2, 3,
|
||||
MotionEvent.ACTION_MOVE,
|
||||
/* downX */(currentWindowX + mMenuHalfWidth)
|
||||
+ /* offsetXToScreenRightSide */ mMenuHalfWidth,
|
||||
/* downY */ (currentWindowY + mMenuHalfHeight));
|
||||
final MotionEvent upEvent =
|
||||
mMotionEventHelper.obtainMotionEvent(4, 5,
|
||||
MotionEvent.ACTION_UP,
|
||||
/* downX */(currentWindowX + mMenuHalfWidth)
|
||||
+ /* offsetXToScreenRightSide */ mMenuHalfWidth,
|
||||
/* downY */ (currentWindowY + mMenuHalfHeight));
|
||||
mListView.dispatchTouchEvent(downEvent);
|
||||
mListView.dispatchTouchEvent(moveEvent);
|
||||
mListView.dispatchTouchEvent(upEvent);
|
||||
|
||||
assertThat(mMenuView.mShapeType).isEqualTo(/* halfOval */ 1);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
mInterceptMotionEvent = null;
|
||||
mMotionEventHelper.recycleEvents();
|
||||
}
|
||||
|
||||
private class TestAccessibilityFloatingMenu extends AccessibilityFloatingMenuView {
|
||||
TestAccessibilityFloatingMenu(Context context, RecyclerView listView) {
|
||||
super(context, listView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
|
||||
@NonNull MotionEvent event) {
|
||||
final boolean intercept = super.onInterceptTouchEvent(recyclerView, event);
|
||||
|
||||
if (intercept) {
|
||||
mInterceptMotionEvent = event;
|
||||
}
|
||||
|
||||
return intercept;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.android.systemui.accessibility.floatingmenu;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.internal.accessibility.dialog.AccessibilityTarget;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.SysuiTestCase;
|
||||
import com.android.systemui.accessibility.floatingmenu.AccessibilityTargetAdapter.ViewHolder;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Tests for {@link AccessibilityTargetAdapter}. */
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
public class AccessibilityTargetAdapterTest extends SysuiTestCase {
|
||||
@Mock
|
||||
private AccessibilityTarget mAccessibilityTarget;
|
||||
|
||||
@Mock
|
||||
private Drawable mIcon;
|
||||
|
||||
@Mock
|
||||
private Drawable.ConstantState mConstantState;
|
||||
|
||||
private ViewHolder mViewHolder;
|
||||
private AccessibilityTargetAdapter mAdapter;
|
||||
private final List<AccessibilityTarget> mTargets = new ArrayList<>();
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
mTargets.add(mAccessibilityTarget);
|
||||
mAdapter = new AccessibilityTargetAdapter(mTargets);
|
||||
|
||||
final View root = LayoutInflater.from(mContext).inflate(
|
||||
R.layout.accessibility_floating_menu_item, null);
|
||||
mViewHolder = new ViewHolder(root);
|
||||
when(mAccessibilityTarget.getIcon()).thenReturn(mIcon);
|
||||
when(mIcon.getConstantState()).thenReturn(mConstantState);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onBindViewHolder_setIconWidthHeight_matchResult() {
|
||||
final int iconWidthHeight = 50;
|
||||
mAdapter.setIconWidthHeight(iconWidthHeight);
|
||||
|
||||
mAdapter.onBindViewHolder(mViewHolder, 0);
|
||||
final int actualIconWith = mViewHolder.mIconView.getLayoutParams().width;
|
||||
|
||||
assertThat(actualIconWith).isEqualTo(iconWidthHeight);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user