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_camera">#81C995</color> <!-- g300 -->
|
||||||
<color name="privacy_circle_microphone_location">#FCAD70</color> <!--o300 -->
|
<color name="privacy_circle_microphone_location">#FCAD70</color> <!--o300 -->
|
||||||
|
|
||||||
|
<!-- Accessibility floating menu -->
|
||||||
|
<color name="accessibility_floating_menu_background">#B3000000</color> <!-- 70% -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -274,4 +274,8 @@
|
|||||||
<!-- TODO(b/178093014) Colors for privacy dialog. These should be changed to the new palette -->
|
<!-- 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_camera">#1E8E3E</color> <!-- g600 -->
|
||||||
<color name="privacy_circle_microphone_location">#E8710A</color> <!--o600 -->
|
<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>
|
</resources>
|
||||||
|
|||||||
@@ -1356,6 +1356,19 @@
|
|||||||
<dimen name="people_space_image_radius">20dp</dimen>
|
<dimen name="people_space_image_radius">20dp</dimen>
|
||||||
<dimen name="people_space_widget_background_padding">6dp</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>
|
<dimen name="rounded_slider_height">44dp</dimen>
|
||||||
<!-- rounded_slider_height / 2 -->
|
<!-- rounded_slider_height / 2 -->
|
||||||
<dimen name="rounded_slider_corner_radius">22dp</dimen>
|
<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.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
class MotionEventHelper {
|
public class MotionEventHelper {
|
||||||
@GuardedBy("this")
|
@GuardedBy("this")
|
||||||
private final List<MotionEvent> mMotionEvents = new ArrayList<>();
|
private final List<MotionEvent> mMotionEvents = new ArrayList<>();
|
||||||
|
|
||||||
void recycleEvents() {
|
public void recycleEvents() {
|
||||||
for (MotionEvent event:mMotionEvents) {
|
for (MotionEvent event:mMotionEvents) {
|
||||||
event.recycle();
|
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) {
|
float y) {
|
||||||
MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, x, y, 0);
|
MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, x, y, 0);
|
||||||
synchronized (this) {
|
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