Освой Android играючи
/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000
Основы
Отдельный поток
Пунктирный ProgressBar
Режим Indeterminate
Вертикальный индикатор прогресса
Круговые индикаторы прогресса
Индикатор с текстом
Программное создание индикатора
Компонент ProgressBar (Horizontal) (Индикатор прогресса) применяется в тех случаях, когда пользователю нужно показать, что программа не зависла, а выполняет продолжительную работу.
Находится в категории Widgets.
Студия предлагает несколько видов индикаторов: ProgressBar и ProgressBar (Horizontal). В свою очередь компонент имеет несколько модификаций и их отличия заложены в стилях, если посмотреть XML-описание компонентов. Стили постоянно меняются, поэтому проще выбрать атрибут style и посмотреть предлагаемые варианты из выпадающего списка на текущий момент.
Методы для работы с ProgressBar:
Горизонтальный индикатор обычно используют, чтобы показать индикатор в действии. При длительной операции пользователь видит, что полоска индикатора постоянно увеличивается.
Нужно задать верхнюю границу диапазона max и начальное значение progress и затем программно работать с этими данными.
Атрибут android:secondaryProgress="50" создаёт эффект буфера, когда рисуется ещё одна полоска, опережающая основной цвет. Ей можно управлять через метод setSecondaryProgress().
Выполнение длительных операций предпочтительнее производить в отдельном потоке. Android имеет специальный класс Handler для создания фоновых потоков и их взаимодействия с пользовательским интерфейсом. Либо используется AsyncTask и другие способы выполнения в другом потоке.
Запустим индикатор через кнопку.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:padding="24dp"
android:text="0"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Start"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/progressBar" />
</androidx.constraintlayout.widget.ConstraintLayout>
Будем усыплять поток на небольшой промежуток времени.
// Kotlin
package ru.alexanderklimov.progress
import android.os.Bundle
import android.os.Handler
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
private var progressStatus = 0
private var handler = Handler()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
Thread(Runnable {
while (progressStatus < 100){
// update progress status
progressStatus +=1
// sleep the thread for 100 milliseconds
Thread.sleep(100)
// update the progress bar
handler.post {
progressBar.progress = progressStatus
textView.text = "$progressStatus"
}
}
}).start()
}
}
}
Часто нужно выводить не круглое число 100, а что-то другое (размер файла). Тогда нужно сделать небольшие вычисления, чтобы выводились данные в процентах.
Допустим размер файла 365 Мб. Запускаем и смотрим - вся жизнь пробежала перед глазами, ну или год.
// Kotlin
button.setOnClickListener {
button.isEnabled = false
val fileSize = 365
progressBar.max = fileSize
Thread(Runnable {
while (progressStatus < fileSize){
// update progress status
progressStatus +=1
// sleep the thread for 100 milliseconds
Thread.sleep(100)
// update the progress bar
handler.post {
progressBar.progress = progressStatus
val percentage = (progressStatus.toDouble() / fileSize * 100).toInt()
textView.text = "Downloaded $progressStatus ($percentage%)"
if (progressStatus == fileSize) {
button.isEnabled = true
}
}
}
}).start()
}
Изменим обычную линию на пунктир. Создадим файл res/drawable/dash_progressbar.xml.
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/background">
<shape android:shape="line">
<stroke
android:width="3dp"
android:color="#FFDDAA"
android:dashWidth="20dp"
android:dashGap="7dp" />
</shape>
</item>
<item android:id="@+id/progress">
<clip
android:clipOrientation="horizontal"
android:gravity="start">
<shape android:shape="line">
<stroke
android:width="3dp"
android:color="#2299FF"
android:dashWidth="21dp"
android:dashGap="7dp" />
</shape>
</clip>
</item>
</layer-list>
Подключим через атрибут progressDrawable.
<ProgressBar
...
android:progressDrawable="@drawable/dash_progressbar" />
Существует режим неопределённости (indeterminate), когда индикатор работает постоянно. Для этого нужно установить соответствующие атрибуты.
<ProgressBar
...
android:indeterminate="true"
android:indeterminateBehavior="cycle"
android:indeterminateOnly="true"
android:indeterminateTint="#8BC34A"
android:indeterminateTintMode="src_in"
При запуске индикатор прогресса без всякого кода будет выводить бесконечную анимацию.
Переключение между режимами программно - метод setIndeterminate(boolean indeterminate).
Аккуратнее с атрибутом indeterminateOnly, если он будет установлен, то анимацию программно уже не приостановить. В этом случае просто не используйте атрибут. Несмотря на анимацию, индикатор по прежнему меняет свои показания. Если брать предыдущий пример с процентами и установить атрибуты, указанные выше (кроме indeterminateOnly), то программно можно остановить анимацию следующим образом.
// Kotlin
button.setOnClickListener {
button.isEnabled = false
val fileSize = 65
progressBar.max = fileSize
Thread(Runnable {
while (progressStatus < fileSize){
progressStatus +=1
Thread.sleep(100)
handler.post {
progressBar.progress = progressStatus
val percentage = (progressStatus.toDouble() / fileSize * 100).toInt()
textView.text = "Downloaded $progressStatus ($percentage%)"
if (progressStatus == fileSize) {
button.isEnabled = true
}
}
}
}).start()
progressStatus = 0
progressBar.isIndeterminate = false
}
Также часто используют изменение видимости - когда индикатор не нужен, его просто прячут.
Индикатор прогресса работает в стиле Material Design на Lollipop-устройствах и выше. На старых устройствах выводится другой цвет.
Можно задать свои настройки для цвета.
<ProgressBar
...
android:progressTint="#FFEB3B"
android:progressBackgroundTint="#8BC34A"
... />
Или программно.
progressBar.progressTintList = ColorStateList.valueOf(Color.CYAN)
progressBar.progressBackgroundTintList = ColorStateList.valueOf(Color.BLUE)
// новый способ
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
progressBar.progressDrawable.colorFilter =
BlendModeColorFilter(Color.GREEN,BlendMode.SRC_IN)
}else {
progressBar.progressDrawable
.setColorFilter(Color.GREEN,PorterDuff.Mode.SRC_IN)
}
Для тонкой настройки компонента под себя следует использовать файл в ресурсах с использованием LayerDrawable. Файл ресурсов должен использовать системный идентификатор @android:id/background для фона, а также идентификатор @android:id/progress для самого индикатора. Также можно использовать ещё один необязательный идентификатор @android:id/secondaryProgress для дополнительного индикатора. Пример ниже.
По умолчанию используется горизонтальный индикатор. Можно создать собственный индикатор с вертикальной ориентацией.
Рассмотрим пример с обычной вертикальной линией. Создадим файл res/drawable/vert_progressbar.xml.
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/background">
<rotate
android:fromDegrees="90"
android:toDegrees="90">
<shape android:shape="line">
<stroke
android:width="5dp"
android:color="#FFEB3B" />
</shape>
</rotate>
</item>
<item android:id="@+id/progress">
<clip
android:clipOrientation="vertical"
android:gravity="bottom">
<rotate
android:fromDegrees="90"
android:toDegrees="90">
<shape android:shape="line">
<stroke
android:width="5dp"
android:color="#4CAF50" />
</shape>
</rotate>
</clip>
</item>
</layer-list>
Задействуем атрибут android:progressDrawable. Есть определённая сложность размещения компонента на экране. Хотя мы хотим видеть вертикальный индикатор, студия воспринимает его по-прежнему горизонтальным и в режиме дизайна отображает его соответствующим образом. Попробуем следующий вариант.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:padding="24dp"
android:text="0"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="396dp"
android:layout_height="509dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:progressDrawable="@drawable/vert_progressbar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Start"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
</androidx.constraintlayout.widget.ConstraintLayout>
Код можно взять из примера с процентами. Там всё без изменений.
Вариант не слишком удобный, так как не интуитивен при визуальном размещении.
Другой вариант вертикального индикатора, который гораздо лучше справляется со своей задачей. Кроме того, индикатор будет не виде линии, а полноценным жирным прямоугольником. В папке res/drawable подготовим два файла:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:id="@android:id/background">
<shape>
<corners android:radius="5dip" />
<solid android:color="#FFFFFF" />
<stroke
android:width="1dp"
android:color="#FF000000" />
</shape>
</item>
<item android:id="@android:id/progress">
<clip
android:clipOrientation="vertical"
android:gravity="bottom" >
<shape>
<corners android:radius="5dip" />
<solid android:color="#FFAADD" />
<stroke
android:width="1dp"
android:color="#FF000000" />
</shape>
</clip>
</item>
</layer-list>
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:id="@android:id/background">
<shape>
<corners android:radius="5dip" />
<solid android:color="#FFFFFF" />
<stroke
android:width="1dp"
android:color="#FF000000" />
</shape>
</item>
<item android:id="@android:id/progress">
<clip
android:clipOrientation="vertical"
android:gravity="bottom" >
<shape>
<corners android:radius="5dip" />
<solid android:color="#00FF55" />
<stroke
android:width="1dp"
android:color="#FF000000" />
</shape>
</clip>
</item>
</layer-list>
Разница между двумя файлами только в цвете, которым закрашивается индикатор во время работы. Я создал второй файл только для демонстрации программного изменения индикатора - когда значение индикатора достигнет отметки 50, то цвет поменяется на зелёный.
Код можно вставить после строчки из предыдущего примера:
// Kotlin
// код из предыдущего примера
...
// update the progress bar
handler.post {
progressBar.progress = progressStatus
val percentage = (progressStatus.toDouble() / fileSize * 100).toInt()
textView.text = "Downloaded $progressStatus ($percentage%)"
if(percentage > 50){
progressBar.progressDrawable = getDrawable(R.drawable.verticalprogressbar2)
}
if (progressStatus == fileSize) {
button.isEnabled = true
}
}
// Java
if(myProgress >= 50) myProgressBar.setProgressDrawable(getResources().getDrawable(R.drawable.verticalprogressbar2));
Также можно использовать атрибут XML.
<ProgressBar
...
android:progressDrawable="@drawable/myprogressbar"/>
Несмотря на то, что компонент называется горизонтальным индикатором прогресса, мы уже изменили его на вертикальный. Но это не предел, мы можем сделать индикатор круговым.
Сначала покажу неудачный пример, который попался в интернете. Создадим файл res/drawable/circular_progressbar.xml.
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape android:shape="oval">
<stroke
android:width="10dp"
android:color="#DADADA" />
</shape>
</item>
<item android:id="@android:id/progress">
<rotate
android:fromDegrees="180"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="180">
<shape
android:innerRadiusRatio="2.2"
android:shape="ring"
android:thickness="10dp"
android:thicknessRatio="25.0">
<solid android:color="#2299FF" />
</shape>
</rotate>
</item>
</layer-list>
Снова используем атрибут android:progressDrawable. Чтобы пример работал должным образом, нужно следить за размерами индикатора - ширина и высота должны быть одинаковыми. Если забыть об этом, то получим следующее.
Попробуем установить одинаковые размеры, а также добавить пунктир и градиент.
<?xml version="1.0" encoding="UTF-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape android:shape="oval">
<stroke
android:width="8dp"
android:color="#DADADA"
android:dashWidth="5dp"
android:dashGap="5dp" />
</shape>
</item>
<item android:id="@android:id/progress">
<rotate
android:fromDegrees="180"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="180">
<shape
android:innerRadiusRatio="2.1"
android:shape="ring"
android:thickness="9dp"
android:thicknessRatio="25.0">
<solid android:color="#2299FF" />
<gradient
android:angle="90"
android:centerColor="#FFEB3B"
android:endColor="#00BCD4"
android:startColor="#FF5722"
android:type="sweep"
android:useLevel="true" />
</shape>
</rotate>
</item>
</layer-list>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="300dp"
android:layout_height="300dp" />
Для примера с обратным отсчётом я использовал индикатор с двумя drawable-ресурсами, используя shape с другими параметрами.
К сожалению, индикатор не выводит текст во время работы, что не очень удобно. Можно создать свой ProgressBar из TextView или создать свой компонент на основе ProgressBar. Другой простой способ - наложить текстовый блок прямо на индикатор, используя RelativeLayout. Пример для пояснения.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
<RelativeLayout
android:id="@+id/rl_progress_bar_set"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/btn_start"
android:visibility="visible" >
<ProgressBar
android:id="@+id/pb_horizontal"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:max="100" />
<TextView
android:id="@+id/tv_progress_horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/pb_horizontal"
android:background="@android:color/transparent"
android:gravity="center" />
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_below="@id/pb_horizontal"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginTop="10dp" >
<ProgressBar
style="@android:style/Widget.ProgressBar.Large"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="@android:color/background_dark" />
<TextView
android:id="@+id/tv_progress_circle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="@android:color/transparent"
android:gravity="center"
android:textColor="@android:color/white" />
</RelativeLayout>
</RelativeLayout>
<Button
android:id="@+id/btn_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_marginTop="15dp"
android:onClick="onClick"
android:text="Поехали"
android:textSize="25dp" />
</RelativeLayout>
Код, чтобы увидеть работу индикатора. При каждом щелчке кнопки значение индикаторов увеличиваются на 10 процентов.
// Java
package ru.alexanderklimov.progress;
import ...
public class MainActivity extends Activity {
private int progress = 0;
private ProgressBar pbHorizontal;
private TextView tvProgressHorizontal;
private TextView tvProgressCircle;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
pbHorizontal = findViewById(R.id.pb_horizontal);
tvProgressHorizontal = findViewById(R.id.tv_progress_horizontal);
tvProgressCircle = findViewById(R.id.tv_progress_circle);
}
public void onClick(View v) {
progress = progress + 10;
postProgress(progress);
}
private void postProgress(int progress) {
String strProgress = String.valueOf(progress) + " %";
pbHorizontal.setProgress(progress);
if (progress == 0) {
pbHorizontal.setSecondaryProgress(0);
} else {
pbHorizontal.setSecondaryProgress(progress + 5);
}
tvProgressHorizontal.setText(strProgress);
tvProgressCircle.setText(strProgress);
}
}
Данный способ уже устарел, так как через XML намного удобнее. Для общего развития.
// Java
// Зададим цвета для фона и самого индикатора
Drawable background = new ColorDrawable(0xFF373737);
Drawable progress = new ColorDrawable(0xFF00B51C);
ClipDrawable clipProgress = new ClipDrawable(progress, Gravity.LEFT,
ClipDrawable.HORIZONTAL);
// Слои для индикатора
LayerDrawable layerlist = new LayerDrawable(new Drawable[] {
background, clipProgress });
layerlist.setId(0, android.R.id.background);
layerlist.setId(1, android.R.id.progress);
// Создаем новый индикатор с горизонтальным стилем и заданным фоном
ProgressBar progressBar = new ProgressBar(this, null,
android.R.attr.progressBarStyleHorizontal);
progressBar.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT));
progressBar.setProgressDrawable(layerlist);
progressBar.setProgress(76);
LinearLayout linearlayout = (LinearLayout) findViewById(R.id.linearlayout);
linearlayout.addView(progressBar);
Индикаторы прогресса также можно использовать в заголовках приложения у старых устройств, а также в ActionBar и ToolBar у новых устройств.
Компонент постоянно дорабатывается. Например, в Android 8 появились новые методы isAnimating(), getMin(), setMin().
Можно подключить готовую анимацию для визуализации компонента.
private static final TimeInterpolator GAUGE_ANIMATION_INTERPOLATOR = new DecelerateInterpolator(2);
private static final int MAX_LEVEL = 100;
private static final long GAUGE_ANIMATION_DURATION = 5000;
private ProgressBar mProgressBar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mProgressBar = findViewById(R.id.progressBar);
}
// По щелчку кнопки
public void onClick(View view) {
ObjectAnimator animator = ObjectAnimator.ofInt(mProgressBar, "progress", 0, MAX_LEVEL);
animator.setInterpolator(GAUGE_ANIMATION_INTERPOLATOR);
animator.setDuration(GAUGE_ANIMATION_DURATION);
animator.start();
}
Меняем стандартный индикатор через drawable - простой и быстрый способ.
Material ProgressBar - статья про создание собственного индикатора в стиле Material Design
Indeterminate - серия статей на тему создания индикаторов прогресса, ссылка на первую часть.
How to create your own progressBar in Android. – Medium
Make an android custom view, publish and open source.
GitHub - MackHartley/RoundedProgressBar: A customizable, animated progress bar that features rounded corners - индикатор с закруглёнными углами
skydoves/ProgressView: A polished and flexible ProgressView, fully customizable with animations.
GitHub - nipun-birla/BoxLoaderView
GitHub - VladimirWrites/Lemniscate: An easy way to make your progress view nice and sleek.
yuriy-budiyev/circular-progress-bar: Circular progress bar
JustZak/DilatingDotsProgressBar: A customizable indeterminate progress bar
DreaminginCodeZH/MaterialProgressBar: Material Design ProgressBar with consistent appearance
passsy/android-HoloCircularProgressBar
lsjwzh/MaterialLoadingProgressBar
amanjeetsingh150/CoolProgressViews: Android library with collection of cool progress views.
glomadrian/loading-balls: A highly configurable library to do loading progress with animated balls - анимация для длительных операций.
droidchef/tinglingsquares: A delightful progress animation
Индикатор прогресса в виде пирога. Домашняя страница FilipPudak/ProgressPieView
biodunalfet/SlidingSquaresLoader: A simple progress loader