Привет! Меня зовут Андрей Шоколов, я Android-разработчик в компании KODE. Компания «Форвард Лизинг» обратилась к нам с просьбой разработать мобильное приложение на основе готового дизайна.
Прототип содержал дугу, которая по задумке должна была сжиматься в одну строку при прокрутке.
За основу мы решили взять координаторLayout: у нас уже был положительный опыт работы с ним на другом проекте.
Наша команда тоже часто любила посоревноваться, какой Layout лучше — КоординаторLayout или MotionLayout, и сейчас самое время это выяснить.
Сейчас понимаю, что проблема создалась на ровном месте, но узнал только в процессе работы.
В статье я расскажу, с какими 7 трудностями я столкнулся при работе с Координатором и как это сделать за полчаса то, над чем я возился уже несколько дней.
Цель проекта
Мне нужно было создать AppBarLayout с дугой, которая прокручивалась бы в одну строку.Функционал добавлен в существующую Активность приложения Форвард. В качестве корневого элемента для основного Activity был выбран координаторLayout — это ViewGroup, целью которого является координация внутренних элементов представления.
Главный экран мобильного приложения Форвард Лизинг Прежде чем я расскажу вам, что такое координаторLayout и с какими трудностями я столкнулся при реализации, я покажу вам, как это должно было получиться в итоге: Конечный результат
Что такое координаторLayout с поведением по умолчанию
КоординаторLayout — это обычный FrameLayout. Нет, извините: как написано в документации, это сверхмощный FrameLayout!Основной особенностью координатораLayout является его поведение по умолчанию.
Используя поведение, вы можете управлять дочерним представлением.
Думаю, все видели эти красивые анимации, когда Snackbar всплывает снизу, а вместе с ним поднимается вверх FAB (Floating Action Button).
Все работает идеально.
Но именно Behaviors я настрою позже, потому что когда нужно сделать не все по умолчанию, а добавить что-то свое, то возникают некоторые трудности.
Для реализации такой дуги был создан класс RoundedAppBarLayout, наследуемый от AppBarLayout.
Что дано в RoundedAppBarLayout
- Панель инструментов — это наиболее распространенный вид панели инструментов Android.
- ContentView, где может быть что угодно.
- Дуга — это наша дуга.
С какими трудностями я столкнулся и как я их решил?
№1. Прокрутка
У нас есть RoundedAppBarLayout, который содержит панель инструментов, ContentView и Arc. Для каждого элемента представления внутри него можно установить ScrollFlags, которые описывают поведение представления при прокрутке: прокрутка, привязка,expandAlways,expandAlwaysCollapsed,exitUntilCollapsed. Как они работают? AppBarLayout перебирает все имеющиеся у него дочерние представления и смотрит: если установлена прокрутка, то добавляем, что это представление будет прокручиваться, если noScroll — возвращаемся.Если на панели инструментов указано «Прокрутка», она будет прокручиваться, а все, что находится под ней, — нет. Соответственно, если поставить прокрутку на Toolbar и ContentView, то будут прокручиваться только они.
Нам нужно ровно наоборот: чтобы прокручивалась дуга, а не все, что находится над ней.
Решение
Сворачивающаяся панель инструментов.Это позволяет вам оставить панель инструментов вверху и дает вам возможность прокручивать ContentView, Arc и контент ниже.
Это решение работает достаточно хорошо, за исключением того, что панель инструментов всегда должна быть последним элементом CollapsingToolbar. Код
<Эxml version="1.0" encoding="utf-8"?> <merge xmlns:android="http://schemas.android.com/apk/res/android " xmlns:app="http://schemas.android.com/apk/res-auto " xmlns:tools="http://schemas.android.com/tools " android:layout_width="match_parent " android:layout_height="wrap_content " android:background="@color/transparent " tools:parentTag="com.google.android.material.appbar.AppBarLayout "> <com.google.android.material.appbar.CollapsingToolbarLayout android:id="@+id/collapsingToolbar " android:layout_width="match_parent " android:layout_height="wrap_content " android:background="@color/transparent " app:layout_scrollFlags="scroll|snap|exitUntilCollapsed "> </com.google.android.material.appbar.CollapsingToolbarLayout> </merge>
№2. Прозрачные зоны в Дуге и рисовании дуг
Чтобы нарисовать дугу в AppBarLayout, блок Arc должен содержать зоны, которые будут прозрачными.Поэтому мы устанавливаем AppBarLayout прозрачным, как и ContentView: мы не знаем, какого цвета он должен быть.
Но если мы оставим CollapsingToolbar, вложенный в AppBar, без цвета, как наш ContentView, то на выходе мы получим прозрачный прямоугольник.
Решение
Закрасьте панель инструментов CollapsingToolbar в какой-нибудь цвет. Мы выбрали фиолетовый, так как это основной цвет проекта.Благодаря этому решению мы также улучшили анимацию: теперь при прокрутке блок можно затемнить, и это выглядит довольно красиво.
Как это выглядит
Вопрос остается с дугой.
Мы не можем поместить его в CollapsingToolbar, потому что у него свой цвет, а Arc должна быть прозрачной.
Получается, что мы должны разместить дугу в AppBarLayout, но, как мы помним, она не позволяет нам прокручивать всё, что находится ниже.
Решение
Рисуйте на холсте и, прокручивая, придумайте, как нарисовать дугу.
Слева показано, как выглядит структура макета.
Элемент необходимо добавить в CollapsingToolbar. Можно было бы сразу добавить сюда Toolbar, но так как он должен быть последним элементом в CollapsingToolbarLayout, то его придется писать отдельно.
То есть я делаю макет Inflate и добавляю в конец CollapsingToolbarLayout. Код private fun addContentView(){
if (childCount > 1) {
val contentChild = this.getChildAt(1)
removeView(contentChild)
val layoutParams = CollapsingToolbarLayout.LayoutParams(contentChild.layoutParams)
with(contentChild) {
layoutParams.setMargins(
marginStart,
marginTop + toolbarHeight,
marginEnd,
marginBottom
)
}
contentChild.setBackgroundColor(context.getColor(R.color.accent2))
contentChild.layoutParams = layoutParams
collapsingToolbar.addView(contentChild)
}
}
override fun onFinishInflate() {
super.onFinishInflate()
addContentView()
val toolbar = LayoutInflater.from(context)
.
inflate(R.layout.toolbar_default, collapsingToolbar, false)
removeView(toolbar)
collapsingToolbar.addView(toolbar)
}
В результате у нас получился вот такой простой класс, у которого есть атрибуты title, где можно задать иконку для Панели инструментов, добавить ContentView, который находится в самом нашем RoundedAppBarLayout, и нарисовать дугу.
Доступные методы
№3. Пустое место после ContentView
С чем вы столкнулись дальше? После Toolbar и ContentView у нас нет места.По идее туда нужно было бы добавить представление, но вместе с ним при прокрутке появилась бы белая полоса.
Я заранее ожидал такого поведения.
Решение
Расширение AppBarLayout. Я также сохраняю appbarBottom для рисования дуги: сохраняю нижнюю координату ContentView и увеличиваю AppBarLayout до конца блока Arc. Так нам будет легче рисовать.(Спойлер: это решение сыграет со мной злую шутку.
) Расширение AppBarLayout class RoundedAppBarLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppBarLayout(context, attrs, defStyleAttr) {
init {
LayoutInflater.from(context).
inflate(R.layout.appbar_rounded_default, this, true)
initView()
}
private fun initView() {
this.post {
layoutParams = CoordinatorLayout.LayoutParams(width, height + arcHeight)
appbarBottom = height.toFloat()
appbarHalfWidth = width / 2f
}
}
}
Добавляю ContentChild и Toolbar для нормального отображения контента override fun onFinishInflate() {
super.onFinishInflate()
if (childCount > 1) {
val contentChild = this.getChildAt(1)
removeView(contentChild)
val layoutParams = CollapsingToolbarLayout.LayoutParams(contentChild.layoutParams)
with(contentChild) {
layoutParams.setMargins(
marginStart,
marginTop + toolbarHeight,
marginEnd,
marginBottom
)
}
contentChild.setBackgroundColor(context.getColor(R.color.accent2))
contentChild.layoutParams = layoutParams
collapsingToolbar.addView(contentChild)
}
я рисую дугу private fun drawArc(canvas: Canvas) {
val scale = bottom / height.toFloat()
arcPath.apply {
reset()
moveTo(0f, appbarBottom)
quadTo(
appbarHalfWidth,
//divide by 2 due to arc drawing logic
(appbarBottom + (arcHeight * 2)) * scale,
width.toFloat(),
appbarBottom
)
}
canvas.drawPath(arcPath, arcPaint).
apply {
invalidate()
}
}
Нам нужно рассчитать расстояние, когда AppBarLayout полностью и не полностью прокручен.
То есть масштаб = 1 означает, что AppBarLayout полностью развернут, а масштаб = 0 означает, что он свернут. И рисую саму дугу.
Я тут наткнулся на очень интересную логику: если указать по координате y, что крайняя точка дуги должна быть на уровне 48 пикселей, то вершина этой точки будет на уровне 24 пикселей — в два раза меньше.
В итоге нужно умножать на два - решил я, не понимая причины.
Сейчас, пишу эту статью, я понимаю, что использовал функцию, рисующую не дугу, а кривую Безье.
И строится оно не через крайнюю точку, а посередине.
Вы можете использовать Дугу вместо Пути.
Реализация останется прежней, только без умножения на 2. Весь код класса AppBarLayout class RoundedAppBarLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppBarLayout(context, attrs, defStyleAttr) {
private var appbarBottom = 0f
private var appbarHalfWidth = 0f
//region toolbarArc
private val arcHeight = DimensUtils.convertDpToPixel(48f, context)
private val toolbarHeight = DimensUtils.convertDpToPixel(56f, context)
private val arcPaint = Paint().
apply { color = context.getColor(R.color.accent2) style = Paint.Style.FILL isAntiAlias = true } private val arcPath = Path() //endregion init { LayoutInflater.from(context).
inflate(R.layout.appbar_default, this, true) initView() } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) drawArc(canvas) } override fun onFinishInflate() { super.onFinishInflate() if (childCount > 1) { val contentChild = this.getChildAt(1) removeView(contentChild) val layoutParams = CollapsingToolbarLayout.LayoutParams(contentChild.layoutParams) with(contentChild) { layoutParams.setMargins( marginStart, marginTop + toolbarHeight, marginEnd, marginBottom ) } contentChild.setBackgroundColor(context.getColor(R.color.accent2)) contentChild.layoutParams = layoutParams collapsingToolbar.addView(contentChild) } val toolbar = LayoutInflater.from(context) .
inflate(R.layout.toolbar_default, collapsingToolbar, false) removeView(toolbar) collapsingToolbar.addView(toolbar) } private fun initView() { setBackgroundColor(context.getColor(R.color.transparent)) outlineProvider = null this.post { layoutParams = CoordinatorLayout.LayoutParams(width, height + arcHeight) appbarBottom = height.toFloat() appbarHalfWidth = width / 2f } } private fun drawArc(canvas: Canvas) { val scale = bottom / height.toFloat() arcPath.apply { reset() moveTo(0f, appbarBottom) quadTo( appbarHalfWidth, //divide by 2 due to arc drawing logic (appbarBottom + (arcHeight * 2)) * scale, width.toFloat(), appbarBottom ) } canvas.drawPath(arcPath, arcPaint).
apply {
invalidate()
}
}
}
№4. Белая полоса при коллапсе
Когда я порадовался, что все работает хорошо, в основном коде вдруг появился костыль.При прокрутке экрана буквально за пару пикселей до конца появляется белый прямоугольник.
Когда начинаешь его разбирать, понимаешь, что это трюки с поведением AppBarLayout. Так как я обновил ширину всего AppBarLayout (теперь есть AppBarLayout и ширина нашей дуги), то при его сворачивании Layout видит, что AppBarLayout должен быть той же высоты, что и Toolbar и дуга, поэтому он распределяет пространство ровно для этого размера.
Изначально я думал, что раз у AppBarLayout прозрачный фон, проблем не будет, но когда AppBarLayout сворачивается, цвет фона уже не имеет значения.
Во время разработки я не понял, почему свободное место в AppBarLayout остается белым, а не прозрачным.
Я не смог найти этого в коде AppBarLayout. Пожалуй, это тонкости работы с координаторомLayout.
Решение
Я долго думал, как это исправить, и решил воспользоваться методом «инкостализации».
Он заключается в том, что я пишу свой собственный Behavior, и когда он устанавливается в AppBarLayout, вызываю метод setRoundedAppBarBehavior. По сути, я просто добавляю дополнительное поле, чтобы оно могло отображаться поверх AppBarLayout. Код class RoundedAppBarBehavior(context: Context, attrs: AttributeSet) :
AppBarLayout.ScrollingViewBehavior(context, attrs) {
private var isInstalled = false
override fun layoutDependsOn(
parent: CoordinatorLayout,
child: View,
dependency: View
): Boolean {
if (!isInstalled && dependency is BaseAppBarLayout) {
isInstalled = true
child.setRoundedAppBarBehavior()
}
return super.layoutDependsOn(parent, child, dependency)
}
}
fun View.setRoundedAppBarBehavior() {
if (this is ViewGroup) {
val arcHeight = resources.getDimensionPixelSize(R.dimen.toolbar_arc_height)
this.post {
clipToPadding = false
val lp = CoordinatorLayout.LayoutParams(layoutParams)
lp.behavior = AppBarLayout.ScrollingViewBehavior()
lp.setMargins(marginStart, marginTop - arcHeight, marginEnd, marginBottom)
layoutParams = lp
}
}
}
Все снова работает, ура! Как выглядит проблема и решение?
№ 5. AppBar не будет прокручиваться без контента
Оказалось, что каждый AppBarLayout может содержать внутри себя контент. А AppBarLayout думает: внутри меня нет контента, так зачем мне скроллить?Решение
Разделение базового класса на два дочерних класса: RoundedAppBarLayout и SimpleRoundedAppBarLayout. В первый можно поместить контент, но второй останется без него.
Код базового класса AppBarLayout и двух унаследованных: SimpleRoundedAppBarLayout и RoundedAppBarLayout. abstract class BaseAppBarLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppBarLayout(context, attrs, defStyleAttr) {
var title: String? = null
set(value) {
field = value
titleToolbar?.
text = value ?: "" } var navigationIcon: Int? = null var isEnableCollapsingBehaviour: Boolean = false // The value from which the arc is drawn protected var appbarBottom = 0f protected var appbarHalfWidth = 0f //region toolbarArc protected val arcHeight = context.resources.getDimensionPixelSize(R.dimen.toolbar_arc_height) protected val toolbarHeight = context.resources.getDimensionPixelSize(R.dimen.toolbar_height) val nonScrollableHeight = arcHeight + toolbarHeight protected val arcPaint = Paint().
apply { color = context.getColor(R.color.accent2) style = Paint.Style.FILL isAntiAlias = true } protected val arcPath = Path() //endregion var contentChild: View? = null override fun onDraw(canvas: Canvas) { super.onDraw(canvas) drawArc(canvas) } fun updateToolbarTitleMargin( marginStart: Int = 0, marginTop: Int = 0, marginEnd: Int = 0, marginBottom: Int = 0 ) { titleToolbar.layoutParams = Toolbar.LayoutParams(titleToolbar.layoutParams).
apply { setMargins(marginStart, marginTop, marginEnd, marginBottom) } } protected fun initToolbar(toolbar: Toolbar) { navigationIcon?.
let { toolbar.navigationIcon = ContextCompat.getDrawable(context, it) toolbar.setNavigationOnClickListener { (context as? Activity)?.
onBackPressed() } } toolbar.findViewById<TextView>(R.id.titleToolbar)?.
let { textView -> textView.text = title if (navigationIcon != null) { textView.layoutParams = Toolbar.LayoutParams(textView.layoutParams).
apply { setMargins(0, 0, 64.dp, 0) } } } } protected fun initAttrs(attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyle: Int = 0) { context.withStyledAttributes( attrs, R.styleable.RoundedAppBarLayout, defStyleAttr, defStyle ) { val defValue = -1 title = getString(R.styleable.RoundedAppBarLayout_ral_title) getResourceId(R.styleable.RoundedAppBarLayout_ral_navigate_icon, defValue).
apply { if (this != defValue) navigationIcon = this } isEnableCollapsingBehaviour = getBoolean(R.styleable.RoundedAppBarLayout_ral_collapsing_behaviour, false) } } protected open fun drawArc(canvas: Canvas) { val scale = (bottom - nonScrollableHeight) / (height - nonScrollableHeight).
toFloat() contentChild?.
alpha = max(0f, scale - (1 - scale)) if (isEnableCollapsingBehaviour) collapsingBehaviour(scale) arcPath.apply { reset() moveTo(0f, appbarBottom) quadTo( appbarHalfWidth, //multiply by 2 due to arc drawing logic appbarBottom + (arcHeight * 2) * scale, width.toFloat(), appbarBottom ) } canvas.drawPath(arcPath, arcPaint).
apply { invalidate() } } private fun collapsingBehaviour(scale: Float) { titleToolbar?.
alpha = 1 - scale contentChild?.
y = (toolbarHeight - arcHeight).
toFloat().
dp } } class SimpleRoundedAppBarLayout @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : BaseAppBarLayout(context, attrs, defStyleAttr) { init { LayoutInflater.from(context).
inflate(R.layout.appbar_rounded_simple, this, true) initView() initAttrs(attrs, defStyleAttr) } override fun onFinishInflate() { super.onFinishInflate() toolbar.setBackgroundColor(ContextCompat.getColor(context, R.color.accent2)) initToolbar(toolbar) } override fun drawArc(canvas: Canvas) { val scale = (bottom - toolbarHeight) / (height - toolbarHeight).
toFloat() val startDrawingHeight = appbarBottom + arcHeight * (1 - scale) arcPath.apply { reset() moveTo(0f, startDrawingHeight) quadTo( appbarHalfWidth, //multiply by 2 due to arc drawing logic appbarBottom + (arcHeight * 2) * scale, width.toFloat(), startDrawingHeight ) } canvas.drawPath(arcPath, arcPaint).
apply { invalidate() } } private fun initView() { setBackgroundColor(context.getColor(R.color.transparent)) backgroundTintMode = PorterDuff.Mode.OVERLAY outlineProvider = null this.post { appbarBottom = height - arcHeight.toFloat() appbarHalfWidth = width / 2f } } } class RoundedAppBarLayout @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : BaseAppBarLayout(context, attrs, defStyleAttr) { private var isArcDrawn = false init { LayoutInflater.from(context).
inflate(R.layout.appbar_rounded_default, this, true) initView() initAttrs(attrs, defStyleAttr) } override fun onFinishInflate() { super.onFinishInflate() if (childCount > 1) addContentView() val toolbar = LayoutInflater.from(context) .
inflate(R.layout.toolbar_default, collapsingToolbar, false) removeView(toolbar) collapsingToolbar.addView(toolbar) (toolbar as? Toolbar)?.
let { initToolbar(it) }
}
private fun initView() {
setBackgroundColor(context.getColor(R.color.transparent))
backgroundTintMode = PorterDuff.Mode.OVERLAY
outlineProvider = null
this.post {
layoutParams = CoordinatorLayout.LayoutParams(width, height + arcHeight)
appbarBottom = height.toFloat()
appbarHalfWidth = width / 2f
isArcDrawn = true
}
}
private fun addContentView() {
val contentChild = this.getChildAt(1)
removeView(contentChild)
val layoutParams = CollapsingToolbarLayout.LayoutParams(contentChild.layoutParams)
layoutParams.setMargins(
marginStart,
marginTop + toolbarHeight,
marginEnd,
marginBottom
)
contentChild.setBackgroundColor(context.getColor(R.color.accent2))
contentChild.layoutParams = layoutParams
this.contentChild = contentChild
collapsingToolbar.addView(contentChild)
}
}
Панель инструментов, на которой нет содержимого, теперь можно раскрасить.
Мы делаем это потому, что теперь не будет эффекта затемнения, как было в гифке в первом пункте (так как там нет другого контента, кроме Панели инструментов), поэтому мы можем просто задать цвет, и все будет работать нормально.
Здесь я показываю, как это было сделано в итоге.
Схема AppBar Здесь я уже мог добавить ArcView — сам вид дуги — и не переживать о том, что там не будет прозрачной дистанции.
Теперь CollapsingToolbar будет прозрачным, панель инструментов останется цветной, а AppBarLayout не требует расширения.
Сравнение структур макета Единственное, что нам пришлось изменить в коде, это изменить высоту и переделать прорисовку дуги, поскольку поскольку Child там нет, то и затемнять ничего не нужно.
XML-код для SimpleRoundedAppBarLayout и RoundedAppBarLayout <Эxml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android "
xmlns:app="http://schemas.android.com/apk/res-auto "
xmlns:tools="http://schemas.android.com/tools "
android:layout_width="match_parent "
android:layout_height="wrap_content "
android:background="@color/transparent "
android:orientation="vertical "
tools:parentTag="com.google.android.material.appbar.AppBarLayout ">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsingToolbar "
android:layout_width="match_parent "
android:layout_height="wrap_content "
android:background="@color/transparent "
app:layout_scrollFlags="scroll|snap|exitUntilCollapsed ">
<View
android:id="@+id/arcView "
android:layout_width="match_parent "
android:layout_height="48dp "
android:layout_marginTop="56dp "
android:background="@color/transparent " />
<include
android:id="@+id/toolbar "
layout="@layout/toolbar_default"
android:layout_width="match_parent "
android:layout_height="56dp "
android:layout_gravity="top "
app:layout_collapseMode="pin " />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</merge>
<Эxml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android "
xmlns:app="http://schemas.android.com/apk/res-auto "
xmlns:tools="http://schemas.android.com/tools "
android:layout_width="match_parent "
android:layout_height="wrap_content "
android:background="@color/transparent "
tools:parentTag="com.google.android.material.appbar.AppBarLayout ">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsingToolbar "
android:layout_width="match_parent "
android:layout_height="wrap_content "
android:background="@color/accent2 "
app:layout_scrollFlags="scroll|snap|exitUntilCollapsed ">
</com.google.android.material.appbar.CollapsingToolbarLayout>
</merge>
№6. При скрытии и показе AppBarLayout в активности появляется белая полоса
Далее у нас были последствия метода «инкостализации» при настройке видимости.У нас одна деятельность и по ней много фрагментов.
Вверху активности находится RoundedAppBarLayout, а внизу — контейнер для фрагментов.
Если при переключении между фрагментами изменить видимость, появляется белая полоса.
Причина в том, что в дочернем контейнере фрагмента было установлено поле.
Белая полоса – это тот самый край.
При скрытии и показе AppBarLayout в активности появлялась белая полоса.
Решение
Завершите пользовательское поведение и добавьте к нему VisibilityListener. Новый код поведения class RoundedAppBarBehavior(context: Context, attrs: AttributeSet) :
AppBarLayout.ScrollingViewBehavior(context, attrs) {
private var isInstalled = false
override fun layoutDependsOn(
parent: CoordinatorLayout,
child: View,
dependency: View
): Boolean {
if (!isInstalled && dependency is BaseAppBarLayout) {
isInstalled = true
child.setRoundedAppBarBehavior()
addVisibilityListener(dependency, child)
}
return super.layoutDependsOn(parent, child, dependency)
}
private fun addVisibilityListener(appBarLayout: AppBarLayout, child: View) {
var isVisibleSaved = appBarLayout.isVisible
val visibilityListener = ViewTreeObserver.OnGlobalLayoutListener {
if (isVisibleSa
Теги: #Android #Разработка Android #Разработка мобильных приложений #Kotlin #разработка Android #motionlayout #coordinatorlayout
-
Ноутбук Hp Compaq Series 6530B Nb014Ea
19 Oct, 24 -
Первый Набор В Школу Разработки Интерфейсов
19 Oct, 24 -
Используете Ли Вы Местные Торрент-Трекеры?
19 Oct, 24 -
Продать Боковую Платформу
19 Oct, 24