Last active
September 1, 2016 12:15
-
-
Save eungju/8beb6b36ea322d9446eaa19cfc8ffc55 to your computer and use it in GitHub Desktop.
안드로이드 커스텀 레이아웃 단위 테스트
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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