- PVSM.RU - https://www.pvsm.ru -
И снова здравствуйте, коллеги.
И снова я пожаловал к вам с топиком кастомной разметки. На этот раз, я задумал сделать некую “ячейчатую” разметку.
Смысл прост: вся площадь контейнера делится на сетку из квадратных ячеек одинакового размера, и каждый потомок может занять произвольные клетки на этой сетке. Используя подобную разметку можно составлять красивую мозаику из кнопок или картинок, не прибегая к многочисленным вложенным layout’ам. И, поскольку, это специально заточенный для этих целей layout, он будет работать очень шустро.
Прежде, чем мы приступим к нашей основной задаче — измерению и позиционированию потомков, нам нужно задать, какими свойствами они будут обладать.
Каждый из наших потомков будет обладать следующими свойствами:
Давайте определим эти свойства в xml:
<resources>
<declare-styleable name="CellLayout">
<attr name="columns" format="integer" />
<attr name="spacing" format="dimension" />
<attr name="layout_left" format="integer" />
<attr name="layout_top" format="integer" />
<attr name="layout_cellsWidth" format="integer" />
<attr name="layout_cellsHeight" format="integer" />
</declare-styleable>
</resources>
Также мы определили глобальные свойства нашего layout’a — количество колонок (columns) и отступ внутри ячейки (spacing).
Ну что, приступим. Создадим наследника ViewGroup, назовем его CellLayout.
Первое, что мы хотим сделать, это свой LayoutParams, который будет содержать определенные ранее аттрибуты, и который будет присваиваться всем потомкам нашего контейнера.
LayoutParams — это специальный контейнер аттрибутов, который передается каждому потомку контейнера. Каждый контейнер может определить свои нестандартные аттрибуты для потомков (например, RelativeLayout вводит очень много layout_* аттрибутов, доступных потомкам, таких, как layout_toLeftOf). Поэтому каждый тип контейнера может расширять базовый набор. Базовый набор реализован в ViewGroup.LayoutParams (layout_width, layout_height). Кроме этого там же есть немного расширенный вариант — MarginLayoutParams, который добавляет отступы (margins).
public static class LayoutParams extends ViewGroup.LayoutParams {
int top = 0;
int left = 0;
int width = 1;
int height = 1;
public LayoutParams(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CellLayout);
left = a.getInt(R.styleable.CellLayout_layout_left, 0);
top = a.getInt(R.styleable.CellLayout_layout_top, 0);
height = a.getInt(R.styleable.CellLayout_layout_cellsHeight, -1);
width = a.getInt(R.styleable.CellLayout_layout_cellsWidth, -1);
a.recycle();
}
public LayoutParams(ViewGroup.LayoutParams params) {
super(params);
if (params instanceof LayoutParams) {
LayoutParams cellLayoutParams = (LayoutParams) params;
left = cellLayoutParams.left;
top = cellLayoutParams.top;
height = cellLayoutParams.height;
width = cellLayoutParams.width;
}
}
public LayoutParams() {
this(MATCH_PARENT, MATCH_PARENT);
}
public LayoutParams(int width, int height) {
super(width, height);
}
}
Ничего особенного, как видите, тут нет. Подкласc ViewGroup.LayoutParams, хранящий наши свойства и загружающий их из XML.
Теперь, мы хотим попросить наш контейнер передавать экземпляр именно этого класса всем своим потомкам. Для этого нужно переопределить несколько методов ViewGroup:
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new CellLayout.LayoutParams(getContext(), attrs);
}
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof CellLayout.LayoutParams;
}
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new CellLayout.LayoutParams(p);
}
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams();
}
В общем, думаю, названия методов говорят сами за себя.
Теперь, когда мы определили и реализовали атрибуты потомков, нам необходимо добавить измерение нашего компонента и всех его потомков. Для этого мы переопределим метод onMeasure.
Прежде, чем мы продолжим, я обязан рассказать вам немного о методе onMeasure и его особенностях, чтобы дальше не возникало вопросов. Как нетрудно догадаться, это метод, где мы измеряем наш компонент, а также всех его потомков. Но есть особенности.
Их у него две:
На вход в onMeasure поступают не ширина и высота, а так называемые спецификации ширины и высоты, будь они неладны. Спецификация тут — это число, в котором закодированы два числа — размер соответствующей стороны, а также его “режим”. Разумеется, она зависит от того, что мы передаем в layout_width и layout_height нашего компонента. Режим — это один из трех вариантов, как мы должны использовать размер, переданный предком при измерении нашего компонента. Их три:
Стратегия измерения у нас будет следующая:
Давайте посмотрим, как я это реализовал:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int width = 0;
int height = 0;
if (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.EXACTLY) {
width = MeasureSpec.getSize(widthMeasureSpec);
cellSize = (float) (getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) / (float) columns;
} else {
cellSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_CELL_SIZE, getResources()
.getDisplayMetrics());
width = (int) (columns * cellSize);
}
int childCount = getChildCount();
View child;
int maxRow = 0;
for (int i = 0; i < childCount; i++) {
child = getChildAt(i);
LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
int top = layoutParams.top;
int w = layoutParams.width;
int h = layoutParams.height;
int bottom = top + h;
int childWidthSpec = MeasureSpec.makeMeasureSpec((int) (w * cellSize) - spacing * 2,
LayoutParams.MATCH_PARENT);
int childHeightSpec = MeasureSpec.makeMeasureSpec((int) (h * cellSize) - spacing * 2,
LayoutParams.MATCH_PARENT);
child.measure(childWidthSpec, childHeightSpec);
if (bottom > maxRow) {
maxRow = bottom;
}
}
int measuredHeight = Math.round(maxRow * cellSize) + getPaddingTop() + getPaddingBottom();
if (heightMode == MeasureSpec.EXACTLY) {
height = MeasureSpec.getSize(heightMeasureSpec);
} else if (heightMode == MeasureSpec.EXACTLY) {
int atMostHeight = MeasureSpec.getSize(heightMeasureSpec);
height = Math.min(atMostHeight, measuredHeight);
} else {
height = measuredHeight;
}
setMeasuredDimension(width, height);
}
готово, наш компонент теперь меняет свою высоту в зависимости от того, что мы в него поместим. Но где же потомки? Их мы добавим на следующем этапе: компоновка.
Следующий шаг — компоновка. Она выполняется в замечательном методе onLayout. Здесь все гораздо проще — мы проходимся по каждому из наших детишек и позиционируем его в нужной ячейке.
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
View child;
for (int i = 0; i < childCount; i++) {
child = getChildAt(i);
LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
int top = (int) (layoutParams.top * cellSize) + getPaddingTop() + spacing;
int left = (int) (layoutParams.left * cellSize) + getPaddingLeft() + spacing;
int right = (int) ((layoutParams.left + layoutParams.width) * cellSize) + getPaddingLeft() - spacing;
int bottom = (int) ((layoutParams.top + layoutParams.height) * cellSize) + getPaddingTop() - spacing;
child.layout(left, top, right, bottom);
}
}
Как видите, все совсем просто.
Внезапно, вот и всё. На выходе мы получили несложный layout, который позволяет нам размещать элементы относительно сетки. Давайте посмотрим на пример:
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:cl="http://schemas.android.com/apk/res/com.evilduck.celllayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="HardcodedText" >
<com.evilduck.celllayout.CellLayout
android:id="@+id/cell_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
cl:columns="4"
cl:spacing="1dp"
tools:context=".MainActivity" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
cl:layout_cellsHeight="1"
cl:layout_cellsWidth="1"
cl:layout_left="0"
cl:layout_top="0"
android:background="#00FF00"
android:text="View 1" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
cl:layout_cellsHeight="1"
cl:layout_cellsWidth="3"
cl:layout_left="1"
cl:layout_top="0"
android:background="#FFFF00"
android:text="View 2" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
cl:layout_cellsHeight="1"
cl:layout_cellsWidth="1"
cl:layout_left="1"
cl:layout_top="1"
android:background="#FFFFFF"
android:text="View 3" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
cl:layout_cellsHeight="1"
cl:layout_cellsWidth="1"
cl:layout_left="0"
cl:layout_top="1"
android:background="#00FFF0"
android:text="View 4" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
cl:layout_cellsHeight="1"
cl:layout_cellsWidth="2"
cl:layout_left="2"
cl:layout_top="1"
android:background="#00FA00"
android:text="View 5" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
cl:layout_cellsHeight="2"
cl:layout_cellsWidth="3"
cl:layout_left="0"
cl:layout_top="2"
android:background="#AAFFAA"
android:text="View 5" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
cl:layout_cellsHeight="2"
cl:layout_cellsWidth="1"
cl:layout_left="3"
cl:layout_top="2"
android:background="#45CCdd"
android:text="View 6" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
cl:layout_cellsHeight="3"
cl:layout_cellsWidth="1"
cl:layout_left="0"
cl:layout_top="4"
android:background="#FF00FF"
android:text="View 7" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
cl:layout_cellsHeight="3"
cl:layout_cellsWidth="1"
cl:layout_left="1"
cl:layout_top="4"
android:background="#FFFF00"
android:text="View 8" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
cl:layout_cellsHeight="3"
cl:layout_cellsWidth="1"
cl:layout_left="2"
cl:layout_top="4"
android:background="#00FF00"
android:text="View 9" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
cl:layout_cellsHeight="1"
cl:layout_cellsWidth="1"
cl:layout_left="3"
cl:layout_top="4"
android:background="#FFFF00"
android:text="View 10" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
cl:layout_cellsHeight="1"
cl:layout_cellsWidth="1"
cl:layout_left="3"
cl:layout_top="5"
android:background="#FFFFFF"
android:text="View 11" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
cl:layout_cellsHeight="1"
cl:layout_cellsWidth="1"
cl:layout_left="3"
cl:layout_top="6"
android:background="#555555"
android:text="View 12" />
</com.evilduck.celllayout.CellLayout>
</ScrollView>
Результат:
Ура, вроде то, что мы хотели.
Как видите, создание своего layout’a — задача не такая уж и сложная, как может показаться. Конечно, я ни в коем случае не призываю вас изобретать свои велосипеды, но, иногда, возникает необходимость, и лучше знать, как это работает, чем не знать, не так ли?
Как обычно, все исходники доступны на моем гитхабе [1].
Я там также, просто для развлечения, добавил анимированное перемешивание элементов. Настроение было хорошее.
Спасибо за внимание.
Автор: evilduck
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/android/42041
Ссылки в тексте:
[1] гитхабе: https://github.com/TheHiddenDuck/cell-layout
[2] Источник: http://habrahabr.ru/post/191842/
Нажмите здесь для печати.