1 /* 2 * Copyright 2018 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 package androidx.recyclerview.widget; 17 18 import static androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL; 19 import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL; 20 21 import android.content.Context; 22 import android.graphics.Color; 23 import android.util.AttributeSet; 24 import android.view.View; 25 import android.view.ViewGroup; 26 27 import androidx.annotation.NonNull; 28 import androidx.core.util.Pair; 29 30 import org.hamcrest.BaseMatcher; 31 import org.hamcrest.Description; 32 33 import java.util.ArrayList; 34 import java.util.Collections; 35 import java.util.List; 36 import java.util.concurrent.atomic.AtomicLong; 37 38 abstract public class BaseWrapContentWithAspectRatioTest extends BaseRecyclerViewInstrumentationTest { 39 final BaseWrapContentTest.WrapContentConfig mWrapContentConfig; 40 41 protected BaseWrapContentWithAspectRatioTest( 42 BaseWrapContentTest.WrapContentConfig wrapContentConfig) { 43 mWrapContentConfig = wrapContentConfig; 44 } 45 46 int getSize(View view, int orientation) { 47 if (orientation == VERTICAL) { 48 return view.getHeight(); 49 } 50 return view.getWidth(); 51 } 52 53 static class LoggingView extends View { 54 55 MeasureBehavior mBehavior; 56 57 public void setBehavior(MeasureBehavior behavior) { 58 mBehavior = behavior; 59 } 60 61 public LoggingView(Context context) { 62 super(context); 63 } 64 65 public LoggingView(Context context, AttributeSet attrs) { 66 super(context, attrs); 67 } 68 69 public LoggingView(Context context, AttributeSet attrs, int defStyleAttr) { 70 super(context, attrs, defStyleAttr); 71 } 72 73 public LoggingView(Context context, AttributeSet attrs, int defStyleAttr, 74 int defStyleRes) { 75 super(context, attrs, defStyleAttr, defStyleRes); 76 } 77 78 @Override 79 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 80 mBehavior.onMeasure(this, widthMeasureSpec, heightMeasureSpec); 81 } 82 83 @Override 84 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 85 super.onLayout(changed, left, top, right, bottom); 86 mBehavior.onLayout(changed, left, top, right, bottom); 87 } 88 89 public void setMeasured(int w, int h) { 90 setMeasuredDimension(w, h); 91 } 92 93 public void prepareLayoutParams() { 94 mBehavior.setLayoutParams(this); 95 } 96 } 97 98 static class AspectRatioMeasureBehavior extends MeasureBehavior { 99 100 Float ratio; 101 int control; 102 103 public AspectRatioMeasureBehavior(int desiredW, int desiredH, int wMode, int hMode) { 104 super(desiredW, desiredH, wMode, hMode); 105 } 106 107 public AspectRatioMeasureBehavior aspectRatio(int control, float ratio) { 108 this.control = control; 109 this.ratio = ratio; 110 return this; 111 } 112 113 @Override 114 public void onMeasure(LoggingView view, int wSpec, 115 int hSpec) { 116 super.onMeasure(view, wSpec, hSpec); 117 if (control == VERTICAL) { 118 view.setMeasured(getSecondary(view.getMeasuredHeight()), 119 view.getMeasuredHeight()); 120 } else if (control == HORIZONTAL) { 121 view.setMeasured(view.getMeasuredWidth(), 122 getSecondary(view.getMeasuredWidth())); 123 } 124 } 125 126 public int getSecondary(int controlSize) { 127 return (int) (controlSize * ratio); 128 } 129 } 130 131 static class MeasureBehavior { 132 private static final AtomicLong idCounter = new AtomicLong(0); 133 public List<Pair<Integer, Integer>> measureSpecs = new ArrayList<>(); 134 public List<Pair<Integer, Integer>> layouts = new ArrayList<>(); 135 int desiredW, desiredH; 136 final long mId = idCounter.incrementAndGet(); 137 138 ViewGroup.MarginLayoutParams layoutParams; 139 140 public MeasureBehavior(int desiredW, int desiredH, int wMode, int hMode) { 141 this.desiredW = desiredW; 142 this.desiredH = desiredH; 143 layoutParams = new ViewGroup.MarginLayoutParams( 144 wMode, hMode 145 ); 146 } 147 148 public MeasureBehavior withMargins(int left, int top, int right, int bottom) { 149 layoutParams.leftMargin = left; 150 layoutParams.topMargin = top; 151 layoutParams.rightMargin = right; 152 layoutParams.bottomMargin = bottom; 153 return this; 154 } 155 156 public long getId() { 157 return mId; 158 } 159 160 public void onMeasure(LoggingView view, int wSpec, int hSpec) { 161 measureSpecs.add(new Pair<>(wSpec, hSpec)); 162 view.setMeasured( 163 RecyclerView.LayoutManager.chooseSize(wSpec, desiredW, 0), 164 RecyclerView.LayoutManager.chooseSize(hSpec, desiredH, 0)); 165 } 166 167 public int getSpec(int position, int orientation) { 168 if (orientation == VERTICAL) { 169 return measureSpecs.get(position).second; 170 } else { 171 return measureSpecs.get(position).first; 172 } 173 } 174 175 public void setLayoutParams(LoggingView view) { 176 view.setLayoutParams(layoutParams); 177 } 178 179 public void onLayout(boolean changed, int left, int top, int right, int bottom) { 180 if (changed) { 181 layouts.add(new Pair<>(right - left, bottom - top)); 182 } 183 } 184 } 185 186 187 static class WrapContentViewHolder extends RecyclerView.ViewHolder { 188 189 LoggingView mView; 190 191 public WrapContentViewHolder(ViewGroup parent) { 192 super(new LoggingView(parent.getContext())); 193 mView = (LoggingView) itemView; 194 mView.setBackgroundColor(Color.GREEN); 195 } 196 } 197 198 static class WrapContentAdapter extends RecyclerView.Adapter<WrapContentViewHolder> { 199 200 List<MeasureBehavior> behaviors = new ArrayList<>(); 201 202 public WrapContentAdapter(MeasureBehavior... behaviors) { 203 Collections.addAll(this.behaviors, behaviors); 204 } 205 206 @Override 207 public WrapContentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 208 return new WrapContentViewHolder(parent); 209 } 210 211 @Override 212 public void onBindViewHolder(@NonNull WrapContentViewHolder holder, int position) { 213 holder.mView.setBehavior(behaviors.get(position)); 214 holder.mView.prepareLayoutParams(); 215 } 216 217 @Override 218 public int getItemCount() { 219 return behaviors.size(); 220 } 221 } 222 223 static class MeasureSpecMatcher extends BaseMatcher<Integer> { 224 225 private boolean checkSize = false; 226 private boolean checkMode = false; 227 private int mSize; 228 private int mMode; 229 230 public static MeasureSpecMatcher is(int size, int mode) { 231 MeasureSpecMatcher matcher = new MeasureSpecMatcher(size, mode); 232 matcher.checkSize = true; 233 matcher.checkMode = true; 234 return matcher; 235 } 236 237 public static MeasureSpecMatcher size(int size) { 238 MeasureSpecMatcher matcher = new MeasureSpecMatcher(size, 0); 239 matcher.checkSize = true; 240 matcher.checkMode = false; 241 return matcher; 242 } 243 244 public static MeasureSpecMatcher mode(int mode) { 245 MeasureSpecMatcher matcher = new MeasureSpecMatcher(0, mode); 246 matcher.checkSize = false; 247 matcher.checkMode = true; 248 return matcher; 249 } 250 251 private MeasureSpecMatcher(int size, int mode) { 252 mSize = size; 253 mMode = mode; 254 255 } 256 257 @Override 258 public boolean matches(Object item) { 259 if (item == null) { 260 return false; 261 } 262 Integer intValue = (Integer) item; 263 final int size = View.MeasureSpec.getSize(intValue); 264 final int mode = View.MeasureSpec.getMode(intValue); 265 if (checkSize && size != mSize) { 266 return false; 267 } 268 if (checkMode && mode != mMode) { 269 return false; 270 } 271 return true; 272 } 273 274 @Override 275 public void describeMismatch(Object item, Description description) { 276 Integer intValue = (Integer) item; 277 final int size = View.MeasureSpec.getSize(intValue); 278 final int mode = View.MeasureSpec.getMode(intValue); 279 if (checkSize && size != mSize) { 280 description.appendText(" Expected size was ").appendValue(mSize) 281 .appendText(" but received size is ").appendValue(size); 282 } 283 if (checkMode && mode != mMode) { 284 description.appendText(" Expected mode was ").appendValue(modeName(mMode)) 285 .appendText(" but received mode is ").appendValue(modeName(mode)); 286 } 287 } 288 289 @Override 290 public void describeTo(Description description) { 291 if (checkSize) { 292 description.appendText(" Measure spec size:").appendValue(mSize); 293 } 294 if (checkMode) { 295 description.appendText(" Measure spec mode:").appendValue(modeName(mMode)); 296 } 297 } 298 299 private static String modeName(int mode) { 300 switch (mode) { 301 case View.MeasureSpec.AT_MOST: 302 return "at most"; 303 case View.MeasureSpec.EXACTLY: 304 return "exactly"; 305 default: 306 return "unspecified"; 307 } 308 } 309 } 310 } 311