Skip to content

Instantly share code, notes, and snippets.

@eungju
Last active September 1, 2016 12:15
Show Gist options
  • Save eungju/8beb6b36ea322d9446eaa19cfc8ffc55 to your computer and use it in GitHub Desktop.
Save eungju/8beb6b36ea322d9446eaa19cfc8ffc55 to your computer and use it in GitHub Desktop.
안드로이드 커스텀 레이아웃 단위 테스트
/**
* https://github.com/ultimate-deej/FlowLayout-for-Android/blob/master/src/org/deejdev/android/FlowLayout.java
*/
public class FlowLayout extends ViewGroup {
public static final int LEFT_TO_RIGHT = 0;
public static final int TOP_DOWN = 1;
public static final int RIGHT_TO_LEFT = 2;
public static final int BOTTOM_UP = 3;
private int mGravity;
private int mElementSpacing;
private int mLineSpacing;
private int mFlowDirection;
private int mMaxLines;
private FlowLayoutAlgorithm layoutSpec;
public FlowLayout(Context context) {
super(context);
}
public FlowLayout(Context context, AttributeSet attributeSet) {
super(context, attributeSet, 0);
initFromAttributes(context, attributeSet);
}
public FlowLayout(Context context, AttributeSet attributeSet, int defStyle) {
super(context, attributeSet, defStyle);
initFromAttributes(context, attributeSet);
}
private boolean isHorizontal() {
return mFlowDirection == LEFT_TO_RIGHT || mFlowDirection == RIGHT_TO_LEFT;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
final int widthSize = (widthMode == MeasureSpec.UNSPECIFIED ? Integer.MAX_VALUE : MeasureSpec.getSize(widthMeasureSpec)) - getPaddingLeft() - getPaddingRight();
final int heightSize = (heightMode == MeasureSpec.UNSPECIFIED ? Integer.MAX_VALUE : MeasureSpec.getSize(heightMeasureSpec)) - getPaddingTop() - getPaddingBottom();
final boolean isWrapContentWidth = widthMode != MeasureSpec.EXACTLY;
final boolean isWrapContentHeight = heightMode != MeasureSpec.EXACTLY;
final boolean horizontal = isHorizontal();
boolean isWrapLength;
int lineLengthLimit;
if (horizontal) {
isWrapLength = isWrapContentWidth;
lineLengthLimit = widthSize;
} else {
isWrapLength = isWrapContentHeight;
lineLengthLimit = heightSize;
}
FlowLayoutAlgorithm.ChildSpec[] childSpecs = new FlowLayoutAlgorithm.ChildSpec[getChildCount()];
for (int i = 0, childCount = getChildCount(); i < childCount; i++) {
View child = getChildAt(i);
LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
int childWidthMeasureSpec = makeMeasureSpec(childLayoutParams.width, widthSize, isWrapContentWidth);
int childHeightMeasureSpec = makeMeasureSpec(childLayoutParams.height, heightSize, isWrapContentHeight);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
int childAxisSize;
int childCrossAxisSize;
if (horizontal) {
childAxisSize = child.getMeasuredWidth();
childCrossAxisSize = child.getMeasuredHeight();
} else {
childAxisSize = child.getMeasuredHeight();
childCrossAxisSize = child.getMeasuredWidth();
}
FlowLayoutAlgorithm.ChildSpec childSpec = new FlowLayoutAlgorithm.ChildSpec(childAxisSize, childCrossAxisSize);
childSpecs[i] = childSpec;
childLayoutParams.mLength = childSpec.getAxisSize();
childLayoutParams.mThickness = childSpec.getCrossAxisSize();
}
FlowLayoutAlgorithm.Layout layout = layoutSpec.layout(lineLengthLimit, childSpecs);
for (int i = 0, childCount = getChildCount(); i < childCount; i++) {
View child = getChildAt(i);
LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
FlowLayoutAlgorithm.ChildLayout childLayout = layout.getChildren().get(i);
childLayoutParams.mDepth = childLayout.getAxisOffset();
childLayoutParams.mPos = childLayout.getCrossAxisOffset();
}
//TODO: gravity
//adjustDepths(lines, isWrapLength ? totalLength : lineLengthLimit);
if (horizontal) {
this.setMeasuredDimension(
(isWrapContentWidth ? layout.getAxisSize() : widthSize) + getPaddingLeft() + getPaddingRight(),
(isWrapContentHeight ? layout.getCrossAxisSize() : heightSize) + getPaddingTop() + getPaddingBottom());
} else {
this.setMeasuredDimension(
(isWrapContentWidth ? layout.getCrossAxisSize() : widthSize) + getPaddingLeft() + getPaddingRight(),
(isWrapContentHeight ? layout.getAxisSize() : heightSize) + getPaddingTop() + getPaddingBottom());
}
}
private static int makeMeasureSpec(int size, int parentSize, boolean parentWrapContent) {
int childMeasureSpec;
if (size >= 0) {
childMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
} else if (parentWrapContent || size == ViewGroup.LayoutParams.WRAP_CONTENT) {
childMeasureSpec = MeasureSpec.makeMeasureSpec(parentSize, MeasureSpec.AT_MOST);
} else {
childMeasureSpec = MeasureSpec.makeMeasureSpec(parentSize, MeasureSpec.EXACTLY);
}
return childMeasureSpec;
}
private void adjustDepths(ArrayList<Pair<ArrayList<LayoutParams>, Integer>> lines, int lineLengthLimit) {
boolean center;
boolean fill;
if (isHorizontal()) {
center = (mGravity & Gravity.CENTER_HORIZONTAL) == Gravity.CENTER_HORIZONTAL;
fill = (mGravity & Gravity.FILL_HORIZONTAL) == Gravity.FILL_HORIZONTAL;
} else {
center = (mGravity & Gravity.CENTER_VERTICAL) == Gravity.CENTER_VERTICAL;
fill = (mGravity & Gravity.FILL_VERTICAL) == Gravity.FILL_VERTICAL;
}
if (!(center || fill)) {
return;
}
for (Pair<ArrayList<LayoutParams>, Integer> lineInfo : lines) {
int emptySpaceAtEnd = lineLengthLimit - lineInfo.second;
ArrayList<LayoutParams> line = lineInfo.first;
if (fill) {
int childCount = line.size();
if (childCount > 1) {
int spacing = emptySpaceAtEnd / (childCount - 1);
for (int i = 1; i < childCount; i++) {
LayoutParams childLayoutParams = line.get(i);
childLayoutParams.mDepth += spacing * i;
}
}
} else {
int spacing = emptySpaceAtEnd / 2;
for (LayoutParams childLayoutParams : line) {
childLayoutParams.mDepth += spacing;
}
}
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
int height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
for (int i = 0, childCount = getChildCount(); i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
layoutChild(child, width, height);
}
}
}
private void layoutChild(View child, int layoutWidth, int layoutHeight) {
LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
int left, top, right, bottom;
switch (mFlowDirection) {
case RIGHT_TO_LEFT:
right = layoutWidth - childLayoutParams.mDepth;
top = childLayoutParams.mPos;
left = layoutWidth - childLayoutParams.mDepth - childLayoutParams.mLength;
bottom = childLayoutParams.mPos + childLayoutParams.mThickness;
break;
case TOP_DOWN:
left = childLayoutParams.mPos;
top = childLayoutParams.mDepth;
right = childLayoutParams.mPos + childLayoutParams.mThickness;
bottom = childLayoutParams.mDepth + childLayoutParams.mLength;
break;
case BOTTOM_UP:
left = childLayoutParams.mPos;
bottom = layoutHeight - childLayoutParams.mDepth;
right = childLayoutParams.mPos + childLayoutParams.mThickness;
top = layoutHeight - childLayoutParams.mDepth - childLayoutParams.mLength;
break;
default:
left = childLayoutParams.mDepth;
top = childLayoutParams.mPos;
right = childLayoutParams.mDepth + childLayoutParams.mLength;
bottom = childLayoutParams.mPos + childLayoutParams.mThickness;
break;
}
child.layout(left + getPaddingLeft(), top + getPaddingTop(), right + getPaddingLeft(), bottom + getPaddingTop());
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
private void initFromAttributes(Context context, AttributeSet attributeSet) {
TypedArray a = context.obtainStyledAttributes(attributeSet, R.styleable.FlowLayout);
try {
mElementSpacing = a.getDimensionPixelSize(R.styleable.FlowLayout_elementSpacing, 0);
mLineSpacing = a.getDimensionPixelSize(R.styleable.FlowLayout_lineSpacing, 0);
mGravity = a.getInt(R.styleable.FlowLayout_android_gravity, Gravity.NO_GRAVITY);
mFlowDirection = a.getInt(R.styleable.FlowLayout_flowDirection, LEFT_TO_RIGHT);
mMaxLines = a.getInt(R.styleable.FlowLayout_android_maxLines, 0);
layoutSpec = new FlowLayoutAlgorithm(mElementSpacing, mLineSpacing, mMaxLines);
} finally {
a.recycle();
}
}
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
private int mLength, mThickness, mDepth, mPos;
public boolean breakLine;
public LayoutParams(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
TypedArray a = context.obtainStyledAttributes(attributeSet, R.styleable.FlowLayout_Layout);
try {
breakLine = a.getBoolean(R.styleable.FlowLayout_Layout_layout_breakLine, false);
} finally {
a.recycle();
}
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.LayoutParams layoutParams) {
super(layoutParams);
}
}
}
package marvin.widget
/**
* TODO:
* * break
* * child visibility
* * gravity
*/
data class FlowLayoutAlgorithm(val axisSpace: Int = 0, val crossAxisSpace: Int = 0,
val maxLines: Int = 0) {
data class ChildSpec(val axisSize: Int, val crossAxisSize: Int)
data class ChildLayout(val axisOffset: Int, val crossAxisOffset: Int)
data class Layout(val axisSize: Int, val crossAxisSize: Int, val children: List<ChildLayout>)
fun layout(axisLimit: Int, childSpecs: Array<ChildSpec>): Layout {
var axisSize: Int = 0
var crossAxisSize: Int = 0
var lines: Int = 0
var lineLength: Int = 0
var lineThickness: Int = 0
var effectiveAxisSpacing: Int = 0
var effectiveCrossAxisSpacing: Int = 0
val childLayouts = childSpecs.mapIndexed { i, childSpec ->
if (lineLength + effectiveAxisSpacing + childSpec.axisSize > axisLimit) {
axisSize = Math.max(axisSize, lineLength)
if (maxLines < 1 || lines < maxLines) {
crossAxisSize += effectiveCrossAxisSpacing + lineThickness
}
lines++
lineLength = 0
lineThickness = 0
effectiveAxisSpacing = 0
effectiveCrossAxisSpacing = crossAxisSpace
}
val offset = lineLength + effectiveAxisSpacing
val crossOffset = crossAxisSize + effectiveCrossAxisSpacing
lineLength += effectiveAxisSpacing + childSpec.axisSize
effectiveAxisSpacing = axisSpace
lineThickness = Math.max(lineThickness, childSpec.crossAxisSize)
ChildLayout(offset, crossOffset)
}
if (lineLength > 0) {
axisSize = Math.max(axisSize, lineLength)
if (maxLines < 1 || lines < maxLines) {
crossAxisSize += effectiveCrossAxisSpacing + lineThickness
}
lines++
}
return Layout(axisSize, crossAxisSize, childLayouts)
}
}
package marvin.widget
import com.google.common.truth.Truth.assertThat
import marvin.widget.FlowLayoutAlgorithm.*
import org.junit.Test
class FlowLayoutTest {
@Test fun empty() {
assertThat(FlowLayoutAlgorithm().layout(10, emptyArray()))
.isEqualTo(Layout(0, 0, emptyList()))
}
@Test fun soleChild() {
assertThat(FlowLayoutAlgorithm().layout(10, arrayOf(ChildSpec(1, 2))))
.isEqualTo(Layout(1, 2, listOf(ChildLayout(0, 0))))
}
@Test fun underAxisLimit() {
assertThat(FlowLayoutAlgorithm().layout(10, arrayOf(ChildSpec(8, 1), ChildSpec(2, 2))))
.isEqualTo(Layout(10, 2, listOf(ChildLayout(0, 0), ChildLayout(8, 0))))
}
@Test fun lineThicknessIsTheThicknessOfTheThickest() {
assertThat(FlowLayoutAlgorithm().layout(10, arrayOf(ChildSpec(1, 2), ChildSpec(2, 1))))
.isEqualTo(Layout(3, 2, listOf(ChildLayout(0, 0), ChildLayout(1, 0))))
}
@Test fun overAxisLimit() {
assertThat(FlowLayoutAlgorithm().layout(10, arrayOf(ChildSpec(9, 2), ChildSpec(2, 1))))
.isEqualTo(Layout(9, 3, listOf(ChildLayout(0, 0), ChildLayout(0, 2))))
}
@Test fun axisSpace() {
assertThat(FlowLayoutAlgorithm(2).layout(10, arrayOf(ChildSpec(6, 1), ChildSpec(2, 2))))
.isEqualTo(Layout(10, 2, listOf(ChildLayout(0, 0), ChildLayout(8, 0))))
}
@Test fun crossAxisSpace() {
assertThat(FlowLayoutAlgorithm(0, 2).layout(10, arrayOf(ChildSpec(9, 2), ChildSpec(2, 1))))
.isEqualTo(Layout(9, 5, listOf(ChildLayout(0, 0), ChildLayout(0, 4))))
}
@Test fun maxLines() {
assertThat(FlowLayoutAlgorithm(0, 1, 1).layout(10, arrayOf(ChildSpec(9, 2), ChildSpec(2, 1), ChildSpec(2, 1))))
.isEqualTo(Layout(9, 2, listOf(ChildLayout(0, 0), ChildLayout(0, 3), ChildLayout(2, 3))))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment