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