1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.accessibility.accessibilitymenu.view; 18 19 import android.content.Context; 20 import android.content.res.Configuration; 21 import android.graphics.Insets; 22 import android.util.DisplayMetrics; 23 import android.view.LayoutInflater; 24 import android.view.View; 25 import android.view.ViewGroup; 26 import android.view.ViewTreeObserver.OnGlobalLayoutListener; 27 import android.view.WindowInsets; 28 import android.view.WindowManager; 29 import android.view.WindowMetrics; 30 import android.widget.GridView; 31 32 import androidx.viewpager.widget.ViewPager; 33 34 import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService; 35 import com.android.systemui.accessibility.accessibilitymenu.R; 36 import com.android.systemui.accessibility.accessibilitymenu.activity.A11yMenuSettingsActivity.A11yMenuPreferenceFragment; 37 import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut; 38 import com.android.systemui.accessibility.accessibilitymenu.view.A11yMenuFooter.A11yMenuFooterCallBack; 39 40 import java.util.ArrayList; 41 import java.util.List; 42 43 /** 44 * This class handles UI for viewPager and footer. 45 * It displays grid pages containing all shortcuts in viewPager, 46 * and handles the click events from footer to switch between pages. 47 */ 48 public class A11yMenuViewPager { 49 50 /** The default index of the ViewPager. */ 51 public static final int DEFAULT_PAGE_INDEX = 0; 52 53 /** 54 * The class holds the static parameters for grid view when large button settings is on/off. 55 */ 56 public static final class GridViewParams { 57 /** Total shortcuts count in the grid view when large button settings is off. */ 58 public static final int GRID_ITEM_COUNT = 9; 59 60 /** The number of columns in the grid view when large button settings is off. */ 61 public static final int GRID_COLUMN_COUNT = 3; 62 63 /** Total shortcuts count in the grid view when large button settings is on. */ 64 public static final int LARGE_GRID_ITEM_COUNT = 4; 65 66 /** The number of columns in the grid view when large button settings is on. */ 67 public static final int LARGE_GRID_COLUMN_COUNT = 2; 68 69 /** 70 * Returns the number of items in the grid view. 71 * 72 * @param context The parent context 73 * @return Grid item count 74 */ getGridItemCount(Context context)75 public static int getGridItemCount(Context context) { 76 return A11yMenuPreferenceFragment.isLargeButtonsEnabled(context) 77 ? LARGE_GRID_ITEM_COUNT 78 : GRID_ITEM_COUNT; 79 } 80 81 /** 82 * Returns the number of columns in the grid view. 83 * 84 * @param context The parent context 85 * @return Grid column count 86 */ getGridColumnCount(Context context)87 public static int getGridColumnCount(Context context) { 88 return A11yMenuPreferenceFragment.isLargeButtonsEnabled(context) 89 ? LARGE_GRID_COLUMN_COUNT 90 : GRID_COLUMN_COUNT; 91 } 92 93 /** 94 * Returns the number of rows in the grid view. 95 * 96 * @param context The parent context 97 * @return Grid row count 98 */ getGridRowCount(Context context)99 public static int getGridRowCount(Context context) { 100 return A11yMenuPreferenceFragment.isLargeButtonsEnabled(context) 101 ? (LARGE_GRID_ITEM_COUNT / LARGE_GRID_COLUMN_COUNT) 102 : (GRID_ITEM_COUNT / GRID_COLUMN_COUNT); 103 } 104 105 /** 106 * Separates a provided list of accessibility shortcuts into multiple sub-lists. 107 * Does not modify the original list. 108 * 109 * @param pageItemCount The maximum size of an individual sub-list. 110 * @param shortcutList The list of shortcuts to be separated into sub-lists. 111 * @return A list of shortcut sub-lists. 112 */ generateShortcutSubLists( int pageItemCount, List<A11yMenuShortcut> shortcutList)113 public static List<List<A11yMenuShortcut>> generateShortcutSubLists( 114 int pageItemCount, List<A11yMenuShortcut> shortcutList) { 115 int start = 0; 116 int end; 117 int shortcutListSize = shortcutList.size(); 118 List<List<A11yMenuShortcut>> subLists = new ArrayList<>(); 119 while (start < shortcutListSize) { 120 end = Math.min(start + pageItemCount, shortcutListSize); 121 subLists.add(shortcutList.subList(start, end)); 122 start = end; 123 } 124 return subLists; 125 } 126 GridViewParams()127 private GridViewParams() {} 128 } 129 130 private final AccessibilityMenuService mService; 131 132 /** 133 * The pager widget, which handles animation and allows swiping horizontally to access previous 134 * and next gridView pages. 135 */ 136 protected ViewPager mViewPager; 137 138 private ViewPagerAdapter<GridView> mViewPagerAdapter; 139 private final List<GridView> mGridPageList = new ArrayList<>(); 140 141 /** The footer, which provides buttons to switch between pages */ 142 protected A11yMenuFooter mA11yMenuFooter; 143 144 /** The shortcut list intended to show in grid pages of viewPager */ 145 private List<A11yMenuShortcut> mA11yMenuShortcutList; 146 147 /** The container layout for a11y menu. */ 148 private ViewGroup mA11yMenuLayout; 149 150 /** Display context for inflating views. */ 151 private Context mDisplayContext; 152 A11yMenuViewPager(AccessibilityMenuService service, Context displayContext)153 public A11yMenuViewPager(AccessibilityMenuService service, Context displayContext) { 154 this.mService = service; 155 this.mDisplayContext = displayContext; 156 } 157 158 /** 159 * Configures UI for view pager and footer. 160 * 161 * @param a11yMenuLayout the container layout for a11y menu 162 * @param shortcutDataList the data list need to show in view pager 163 * @param pageIndex the index of ViewPager to show 164 */ configureViewPagerAndFooter( ViewGroup a11yMenuLayout, List<A11yMenuShortcut> shortcutDataList, int pageIndex)165 public void configureViewPagerAndFooter( 166 ViewGroup a11yMenuLayout, List<A11yMenuShortcut> shortcutDataList, int pageIndex) { 167 this.mA11yMenuLayout = a11yMenuLayout; 168 mA11yMenuShortcutList = shortcutDataList; 169 initViewPager(); 170 initChildPage(); 171 mA11yMenuFooter = new A11yMenuFooter(a11yMenuLayout, mFooterCallbacks); 172 updateFooterState(); 173 registerOnGlobalLayoutListener(); 174 goToPage(pageIndex); 175 } 176 177 /** Initializes viewPager and its adapter. */ initViewPager()178 private void initViewPager() { 179 mViewPager = mA11yMenuLayout.findViewById(R.id.view_pager); 180 mViewPagerAdapter = new ViewPagerAdapter<>(); 181 mViewPager.setAdapter(mViewPagerAdapter); 182 mViewPager.setOverScrollMode(View.OVER_SCROLL_NEVER); 183 mViewPager.addOnPageChangeListener( 184 new ViewPager.OnPageChangeListener() { 185 @Override 186 public void onPageScrollStateChanged(int state) {} 187 188 @Override 189 public void onPageScrolled( 190 int position, float positionOffset, int positionOffsetPixels) {} 191 192 @Override 193 public void onPageSelected(int position) { 194 updateFooterState(); 195 } 196 }); 197 } 198 199 /** Creates child pages of viewPager by the length of shortcuts and initializes them. */ initChildPage()200 private void initChildPage() { 201 if (mA11yMenuShortcutList == null || mA11yMenuShortcutList.isEmpty()) { 202 return; 203 } 204 205 if (!mGridPageList.isEmpty()) { 206 mGridPageList.clear(); 207 } 208 209 // Generate pages by calculating # of items per grid. 210 for (List<A11yMenuShortcut> page : GridViewParams.generateShortcutSubLists( 211 GridViewParams.getGridItemCount(mService), mA11yMenuShortcutList) 212 ) { 213 addGridPage(page); 214 } 215 216 mViewPagerAdapter.set(mGridPageList); 217 } 218 addGridPage(List<A11yMenuShortcut> shortcutDataListInPage)219 private void addGridPage(List<A11yMenuShortcut> shortcutDataListInPage) { 220 LayoutInflater inflater = LayoutInflater.from(mDisplayContext); 221 View view = inflater.inflate(R.layout.grid_view, null); 222 GridView gridView = view.findViewById(R.id.gridview); 223 A11yMenuAdapter adapter = new A11yMenuAdapter( 224 mService, mDisplayContext, shortcutDataListInPage); 225 gridView.setNumColumns(GridViewParams.getGridColumnCount(mService)); 226 gridView.setAdapter(adapter); 227 mGridPageList.add(gridView); 228 } 229 230 /** Updates footer's state by index of current page in view pager. */ updateFooterState()231 private void updateFooterState() { 232 int currentPage = mViewPager.getCurrentItem(); 233 int lastPage = mViewPager.getAdapter().getCount() - 1; 234 mA11yMenuFooter.getPreviousPageBtn().setEnabled(currentPage > 0); 235 mA11yMenuFooter.getNextPageBtn().setEnabled(currentPage < lastPage); 236 } 237 238 private void goToPage(int pageIndex) { 239 if (mViewPager == null) { 240 return; 241 } 242 if ((pageIndex >= 0) && (pageIndex < mViewPager.getAdapter().getCount())) { 243 mViewPager.setCurrentItem(pageIndex); 244 } 245 } 246 247 /** Registers OnGlobalLayoutListener to adjust menu UI by running callback at first time. */ 248 private void registerOnGlobalLayoutListener() { 249 mA11yMenuLayout 250 .getViewTreeObserver() 251 .addOnGlobalLayoutListener( 252 new OnGlobalLayoutListener() { 253 254 boolean mIsFirstTime = true; 255 256 @Override 257 public void onGlobalLayout() { 258 if (!mIsFirstTime) { 259 return; 260 } 261 262 if (mGridPageList.isEmpty()) { 263 return; 264 } 265 266 GridView firstGridView = mGridPageList.get(0); 267 if (firstGridView == null 268 || firstGridView.getChildAt(0) == null) { 269 return; 270 } 271 272 mIsFirstTime = false; 273 274 int gridItemHeight = firstGridView.getChildAt(0) 275 .getMeasuredHeight(); 276 adjustMenuUISize(gridItemHeight); 277 } 278 }); 279 } 280 281 /** 282 * Adjusts menu UI to fit both landscape and portrait mode. 283 * 284 * <ol> 285 * <li>Adjust view pager's height. 286 * <li>Adjust vertical interval between grid items. 287 * <li>Adjust padding in view pager. 288 * </ol> 289 */ 290 private void adjustMenuUISize(int gridItemHeight) { 291 final int rowsInGridView = GridViewParams.getGridRowCount(mService); 292 final int defaultMargin = 293 (int) mService.getResources().getDimension(R.dimen.a11ymenu_layout_margin); 294 final int topMargin = (int) mService.getResources().getDimension(R.dimen.table_margin_top); 295 final int displayMode = mService.getResources().getConfiguration().orientation; 296 int viewPagerHeight = mViewPager.getMeasuredHeight(); 297 298 if (displayMode == Configuration.ORIENTATION_PORTRAIT) { 299 // In portrait mode, we only need to adjust view pager's height to match its 300 // child's height. 301 viewPagerHeight = gridItemHeight * rowsInGridView + defaultMargin + topMargin; 302 } else if (displayMode == Configuration.ORIENTATION_LANDSCAPE) { 303 // In landscape mode, we need to adjust view pager's height to match screen height 304 // and adjust its child too, 305 // because a11y menu layout height is limited by the screen height. 306 DisplayMetrics displayMetrics = mService.getResources().getDisplayMetrics(); 307 float densityScale = (float) displayMetrics.densityDpi 308 / DisplayMetrics.DENSITY_DEVICE_STABLE; 309 View footerLayout = mA11yMenuLayout.findViewById(R.id.footerlayout); 310 // Keeps footer window height unchanged no matter the density is changed. 311 footerLayout.getLayoutParams().height = 312 (int) (footerLayout.getLayoutParams().height / densityScale); 313 // Adjust the view pager height for system bar and display cutout insets. 314 WindowManager windowManager = mService.getSystemService(WindowManager.class); 315 WindowMetrics windowMetric = windowManager.getCurrentWindowMetrics(); 316 Insets windowInsets = windowMetric.getWindowInsets().getInsetsIgnoringVisibility( 317 WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()); 318 viewPagerHeight = 319 windowMetric.getBounds().height() 320 - footerLayout.getLayoutParams().height 321 - windowInsets.bottom; 322 // Sets vertical interval between grid items. 323 int interval = 324 (viewPagerHeight - topMargin - defaultMargin 325 - (rowsInGridView * gridItemHeight)) 326 / (rowsInGridView + 1); 327 for (GridView gridView : mGridPageList) { 328 gridView.setVerticalSpacing(interval); 329 } 330 331 // Sets padding to view pager. 332 final int finalMarginTop = interval + topMargin; 333 mViewPager.setPadding(defaultMargin, finalMarginTop, defaultMargin, defaultMargin); 334 } 335 final ViewGroup.LayoutParams layoutParams = mViewPager.getLayoutParams(); 336 layoutParams.height = viewPagerHeight; 337 mViewPager.setLayoutParams(layoutParams); 338 } 339 340 /** Callback object to handle click events from A11yMenuFooter */ 341 protected A11yMenuFooterCallBack mFooterCallbacks = 342 new A11yMenuFooterCallBack() { 343 @Override 344 public void onLeftButtonClicked() { 345 // Moves to previous page. 346 int targetPage = mViewPager.getCurrentItem() - 1; 347 goToPage(targetPage); 348 updateFooterState(); 349 } 350 351 @Override 352 public void onRightButtonClicked() { 353 // Moves to next page. 354 int targetPage = mViewPager.getCurrentItem() + 1; 355 goToPage(targetPage); 356 updateFooterState(); 357 } 358 }; 359 } 360