1 /* 2 * Copyright (C) 2015 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.calculator2; 18 19 import android.content.Context; 20 import android.graphics.Color; 21 import android.support.v4.view.PagerAdapter; 22 import android.support.v4.view.ViewPager; 23 import android.util.AttributeSet; 24 import android.util.Log; 25 import android.view.GestureDetector; 26 import android.view.MotionEvent; 27 import android.view.View; 28 import android.view.ViewGroup; 29 30 public class CalculatorPadViewPager extends ViewPager { 31 32 private final PagerAdapter mStaticPagerAdapter = new PagerAdapter() { 33 @Override 34 public int getCount() { 35 return getChildCount(); 36 } 37 38 @Override 39 public View instantiateItem(ViewGroup container, final int position) { 40 final View child = getChildAt(position); 41 42 // Set a OnClickListener to scroll to item's position when it isn't the current item. 43 child.setOnClickListener(new OnClickListener() { 44 @Override 45 public void onClick(View v) { 46 setCurrentItem(position, true /* smoothScroll */); 47 } 48 }); 49 // Set an OnTouchListener to always return true for onTouch events so that a touch 50 // sequence cannot pass through the item to the item below. 51 child.setOnTouchListener(new OnTouchListener() { 52 @Override 53 public boolean onTouch(View v, MotionEvent event) { 54 v.onTouchEvent(event); 55 return true; 56 } 57 }); 58 59 // Set an OnHoverListener to always return true for onHover events so that focus cannot 60 // pass through the item to the item below. 61 child.setOnHoverListener(new OnHoverListener() { 62 @Override 63 public boolean onHover(View v, MotionEvent event) { 64 v.onHoverEvent(event); 65 return true; 66 } 67 }); 68 // Make the item focusable so it can be selected via a11y. 69 child.setFocusable(true); 70 // Set the content description of the item which will be used by a11y to identify it. 71 child.setContentDescription(getPageTitle(position)); 72 73 return child; 74 } 75 76 @Override 77 public void destroyItem(ViewGroup container, int position, Object object) { 78 removeViewAt(position); 79 } 80 81 @Override 82 public boolean isViewFromObject(View view, Object object) { 83 return view == object; 84 } 85 86 @Override 87 public float getPageWidth(int position) { 88 return position == 1 ? 7.0f / 9.0f : 1.0f; 89 } 90 91 @Override 92 public CharSequence getPageTitle(int position) { 93 final String[] pageDescriptions = getContext().getResources() 94 .getStringArray(R.array.desc_pad_pages); 95 return pageDescriptions[position]; 96 } 97 }; 98 99 private final OnPageChangeListener mOnPageChangeListener = new SimpleOnPageChangeListener() { 100 @Override 101 public void onPageSelected(int position) { 102 for (int i = getChildCount() - 1; i >= 0; --i) { 103 final View child = getChildAt(i); 104 // Only the "peeking" or covered page should be clickable. 105 child.setClickable(i != position); 106 107 // Prevent clicks and accessibility focus from going through to descendants of 108 // other pages which are covered by the current page. 109 if (child instanceof ViewGroup) { 110 final ViewGroup childViewGroup = (ViewGroup) child; 111 for (int j = childViewGroup.getChildCount() - 1; j >= 0; --j) { 112 childViewGroup.getChildAt(j) 113 .setImportantForAccessibility(i == position 114 ? IMPORTANT_FOR_ACCESSIBILITY_AUTO 115 : IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); 116 } 117 } 118 } 119 } 120 }; 121 122 private final PageTransformer mPageTransformer = new PageTransformer() { 123 @Override 124 public void transformPage(View view, float position) { 125 if (position < 0.0f) { 126 // Pin the left page to the left side. 127 view.setTranslationX(getWidth() * -position); 128 view.setAlpha(Math.max(1.0f + position, 0.0f)); 129 } else { 130 // Use the default slide transition when moving to the next page. 131 view.setTranslationX(0.0f); 132 view.setAlpha(1.0f); 133 } 134 } 135 }; 136 137 private final GestureDetector.SimpleOnGestureListener mGestureWatcher = 138 new GestureDetector.SimpleOnGestureListener() { 139 @Override 140 public boolean onDown(MotionEvent e) { 141 // Return true so calls to onSingleTapUp are not blocked. 142 return true; 143 } 144 145 @Override 146 public boolean onSingleTapUp(MotionEvent ev) { 147 if (mClickedItemIndex != -1) { 148 getChildAt(mClickedItemIndex).performClick(); 149 mClickedItemIndex = -1; 150 return true; 151 } 152 return super.onSingleTapUp(ev); 153 } 154 }; 155 156 private final GestureDetector mGestureDetector; 157 158 private int mClickedItemIndex = -1; 159 CalculatorPadViewPager(Context context)160 public CalculatorPadViewPager(Context context) { 161 this(context, null /* attrs */); 162 } 163 CalculatorPadViewPager(Context context, AttributeSet attrs)164 public CalculatorPadViewPager(Context context, AttributeSet attrs) { 165 super(context, attrs); 166 167 mGestureDetector = new GestureDetector(context, mGestureWatcher); 168 mGestureDetector.setIsLongpressEnabled(false); 169 170 setAdapter(mStaticPagerAdapter); 171 setBackgroundColor(Color.BLACK); 172 setPageMargin(-getResources().getDimensionPixelSize(R.dimen.pad_page_margin)); 173 setPageTransformer(false, mPageTransformer); 174 addOnPageChangeListener(mOnPageChangeListener); 175 } 176 177 @Override onFinishInflate()178 protected void onFinishInflate() { 179 super.onFinishInflate(); 180 181 // Invalidate the adapter's data set since children may have been added during inflation. 182 getAdapter().notifyDataSetChanged(); 183 184 // Let page change listener know about our initial position. 185 mOnPageChangeListener.onPageSelected(getCurrentItem()); 186 } 187 188 @Override onInterceptTouchEvent(MotionEvent ev)189 public boolean onInterceptTouchEvent(MotionEvent ev) { 190 try { 191 // Always intercept touch events when a11y focused since otherwise they will be 192 // incorrectly offset by a11y before being dispatched to children. 193 if (isAccessibilityFocused() || super.onInterceptTouchEvent(ev)) { 194 return true; 195 } 196 197 // Only allow the current item to receive touch events. 198 final int action = ev.getActionMasked(); 199 if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) { 200 // If a child is a11y focused then we must always intercept the touch event 201 // since it will be incorrectly offset by a11y. 202 final int childCount = getChildCount(); 203 for (int childIndex = childCount - 1; childIndex >= 0; --childIndex) { 204 if (getChildAt(childIndex).isAccessibilityFocused()) { 205 mClickedItemIndex = childIndex; 206 return true; 207 } 208 } 209 210 if (action == MotionEvent.ACTION_DOWN) { 211 mClickedItemIndex = -1; 212 } 213 214 // Otherwise if touch is on a non-current item then intercept. 215 final int actionIndex = ev.getActionIndex(); 216 final float x = ev.getX(actionIndex) + getScrollX(); 217 final float y = ev.getY(actionIndex) + getScrollY(); 218 for (int i = childCount - 1; i >= 0; --i) { 219 final int childIndex = getChildDrawingOrder(childCount, i); 220 final View child = getChildAt(childIndex); 221 if (child.getVisibility() == VISIBLE 222 && x >= child.getLeft() && x < child.getRight() 223 && y >= child.getTop() && y < child.getBottom()) { 224 if (action == MotionEvent.ACTION_DOWN) { 225 mClickedItemIndex = childIndex; 226 } 227 return childIndex != getCurrentItem(); 228 } 229 } 230 } 231 232 return false; 233 } catch (IllegalArgumentException e) { 234 Log.e("Calculator", "Error intercepting touch event", e); 235 return false; 236 } 237 } 238 239 @Override onTouchEvent(MotionEvent ev)240 public boolean onTouchEvent(MotionEvent ev) { 241 try { 242 // Allow both the gesture detector and super to handle the touch event so they both see 243 // the full sequence of events. This should be safe since the gesture detector only 244 // handle clicks and super only handles swipes. 245 mGestureDetector.onTouchEvent(ev); 246 return super.onTouchEvent(ev); 247 } catch (IllegalArgumentException e) { 248 Log.e("Calculator", "Error processing touch event", e); 249 return false; 250 } 251 } 252 } 253