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