1/* 2 * Copyright (C) 2014 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 17package com.google.android.apps.common.testing.ui.espresso.action; 18 19import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayingAtLeast; 20import static com.google.common.base.Preconditions.checkArgument; 21 22import com.google.common.base.Optional; 23import com.google.common.collect.Lists; 24import com.google.common.collect.Range; 25 26import android.os.Build; 27import android.view.View; 28import android.widget.AbsListView; 29import android.widget.Adapter; 30import android.widget.AdapterView; 31import android.widget.AdapterViewAnimator; 32import android.widget.AdapterViewFlipper; 33 34import java.util.List; 35 36/** 37 * Implementations of {@link AdapterViewProtocol} for standard SDK Widgets. 38 * 39 */ 40public final class AdapterViewProtocols { 41 42 /** 43 * Consider views which have over this percentage of their area visible to the user 44 * to be fully rendered. 45 */ 46 private static final int FULLY_RENDERED_PERCENTAGE_CUTOFF = 90; 47 48 private AdapterViewProtocols() {} 49 50 private static final AdapterViewProtocol STANDARD_PROTOCOL = new StandardAdapterViewProtocol(); 51 52 /** 53 * Creates an implementation of AdapterViewProtocol that can work with AdapterViews that do not 54 * break method contracts on AdapterView. 55 * 56 */ 57 public static AdapterViewProtocol standardProtocol() { 58 return STANDARD_PROTOCOL; 59 } 60 61 // TODO(user): expandablelistview protocols 62 63 private static final class StandardAdapterViewProtocol implements AdapterViewProtocol { 64 @Override 65 public Iterable<AdaptedData> getDataInAdapterView(AdapterView<? extends Adapter> adapterView) { 66 List<AdaptedData> datas = Lists.newArrayList(); 67 for (int i = 0; i < adapterView.getCount(); i++) { 68 datas.add( 69 new AdaptedData.Builder() 70 .withData(adapterView.getItemAtPosition(i)) 71 .withOpaqueToken(i) 72 .build()); 73 } 74 return datas; 75 } 76 77 @Override 78 public Optional<AdaptedData> getDataRenderedByView(AdapterView<? extends Adapter> adapterView, 79 View descendantView) { 80 if (adapterView == descendantView.getParent()) { 81 int position = adapterView.getPositionForView(descendantView); 82 if (position != AdapterView.INVALID_POSITION) { 83 return Optional.of(new AdaptedData.Builder() 84 .withData(adapterView.getItemAtPosition(position)) 85 .withOpaqueToken(Integer.valueOf(position)) 86 .build()); 87 } 88 } 89 return Optional.absent(); 90 } 91 92 @Override 93 public void makeDataRenderedWithinAdapterView( 94 AdapterView<? extends Adapter> adapterView, AdaptedData data) { 95 checkArgument(data.opaqueToken instanceof Integer, "Not my data: %s", data); 96 int position = ((Integer) data.opaqueToken).intValue(); 97 98 boolean moved = false; 99 // set selection should always work, we can give a little better experience if per subtype 100 // though. 101 if (Build.VERSION.SDK_INT > 7) { 102 if (adapterView instanceof AbsListView) { 103 if (Build.VERSION.SDK_INT > 10) { 104 ((AbsListView) adapterView).smoothScrollToPositionFromTop(position, 105 adapterView.getPaddingTop(), 0); 106 } else { 107 ((AbsListView) adapterView).smoothScrollToPosition(position); 108 } 109 moved = true; 110 } 111 if (Build.VERSION.SDK_INT > 10) { 112 if (adapterView instanceof AdapterViewAnimator) { 113 if (adapterView instanceof AdapterViewFlipper) { 114 ((AdapterViewFlipper) adapterView).stopFlipping(); 115 } 116 ((AdapterViewAnimator) adapterView).setDisplayedChild(position); 117 moved = true; 118 } 119 } 120 } 121 if (!moved) { 122 adapterView.setSelection(position); 123 } 124 } 125 126 @SuppressWarnings("deprecation") 127 @Override 128 public boolean isDataRenderedWithinAdapterView( 129 AdapterView<? extends Adapter> adapterView, AdaptedData adaptedData) { 130 checkArgument(adaptedData.opaqueToken instanceof Integer, "Not my data: %s", adaptedData); 131 int dataPosition = ((Integer) adaptedData.opaqueToken).intValue(); 132 133 if (Range.closed(adapterView.getFirstVisiblePosition(), adapterView.getLastVisiblePosition()) 134 .contains(dataPosition)) { 135 if (adapterView.getFirstVisiblePosition() == adapterView.getLastVisiblePosition()) { 136 // thats a huge element. 137 return true; 138 } else { 139 return isElementFullyRendered(adapterView, 140 dataPosition - adapterView.getFirstVisiblePosition()); 141 } 142 } else { 143 return false; 144 } 145 } 146 147 private boolean isElementFullyRendered(AdapterView<? extends Adapter> adapterView, 148 int childAt) { 149 View element = adapterView.getChildAt(childAt); 150 // Occassionally we'll have to fight with smooth scrolling logic on our definition of when 151 // there is extra scrolling to be done. In particular if the element is the first or last 152 // element of the list, the smooth scroller may decide that no work needs to be done to scroll 153 // to the element if a certain percentage of it is on screen. Ugh. Sigh. Yuck. 154 155 return isDisplayingAtLeast(FULLY_RENDERED_PERCENTAGE_CUTOFF).matches(element); 156 } 157 } 158} 159