Признаюсь: я писал поддельный экран загрузки
На выходных посмотрел видео Алексея Макаренкова с заголовком «Полоса загрузки — не то, чем кажется…», где он рассказывает как разработчики игр мухлюют с полоской загрузки.
Вкратце: полоска загрузки в играх — фейк, могла двигаться как угодно, но движется рывками, человеческое восприятие считает именно такой сценарий загрузки самым правдоподобным, а в плавную загрузку игроки не верят. Лучше один раз увидеть, чем сто раз услышать, вот это видео: Полоса загрузки — не то, чем кажется… (осторожно, присутствует реклама красного банка).
Но если смотреть лень, то дальше Алексей говорит о том, что это и так было предсказуемо — секрет Полишинеля, но об этом никто, как правило, не говорит. Когда люди узнают правду, это их «слегка» удивляет. Более того, в статьях и лекциях девелоперов, даже в тех которые посвящены дизайну экранов загрузки, о фейках не пишут.
И тут я могу попытаться заполнить пробел, и рассказать про то, как создавал фейковый экран загрузки. Нет, я не разработчик игр, однако играми экраны загрузки не ограничиваются. Лично я писал такой муляж для приложения на Silverlight. Как давно, это было, помнит только мутной реки вода: все сроки давности уже прошли, про это приложение, да и про Silverlight, уже все позабыли, так что можно снять гриф секретности, сдуть пыль со старого кода и вспомнить как это было.
Олды тут? Вместо дисклеймера
В публикации будет некрокод, с учётом того, что Silverlight уже не поддерживается, буду исходить из предположения что никто разбираться в этом не желает, постараюсь давать пояснения, достаточные для формирования представления и понимания. Всё-таки статья не про Silverlight, а про то, «как разработчики обманывают с экранами загрузки».
Проблема. Вместо введения
Нам экран загрузки изначально не особо был нужен, и уж тем более не было цели кого-то обманывать. В проекте присутствует индикатор загрузки по умолчанию, он справлялся со своей обязанностью, даже ничего писать не надо, типичный код, по-моему, генерируется при создании проекта:
aspx страница
<%@ Page Language="c#" AutoEventWireup="true" %>
Silverlight Client
Указывается xap-файл, и пока он грузится — идёт индикатор загрузки: вращающееся колесо и число показывающее процент загрузки в зависимости от размера скачиваемого xap-файла: скачалось 2 Мб из 8, покажет 25%. Это поведение по умолчанию — ничего дополнительно писать не надо.
Всё было хорошо пока в один прекрасный день, в который никто ничего не трогал, оно само, размер скачиваемого xap-файл ни стал оцениваться в 0 байт. Само собой файл не стал невесомым, просто, почему-то, при скачивании, кто-то, или что-то, зарезал заголовок с размером файла.
На экране загрузке гордо крутилось колесо с надписью 0%, висели эти 0% относительно долго, обычно загрузка занимала пару минут, и потом резко 100%…
На поиск решения потратили день — решить с наскоку не получилось. С другой стороны жалко тратить на это время: ошибки то вроде нет, ну и что что полоска загрузки висит на нуле долгое время, — на работе приложения это не сказывается никак, да неприятно, поэтому проблему не стали сбрасывать, но решили что приоритет у неё невысокий и будет она решаться в свободное от других задач время.
Прошла неделя, периодически к этой задаче возвращались, но решение найдено не было.
Прошло ещё некоторое время. И тут начали возмущаться уже пользователи, мол висит индикатор загрузки, кэш чистили, куки чистили, компьютер перезагружали, браузер меняли, а он на нуле и ничего не грузит, и у всех такое дело. Что с приложением стало? Объясняли что так мол и так, — не надо суеты, ждите и всё будет. Пользователи набирались терпения, убедились что всё работает, но осадочек остался, и чтобы не разводить панику надо было индикатор загрузки чинить.
Вернулись к задаче, прошла ещё пара дней, а причину почему оценка размера xap-файла равна нулю, мы не нашли и даже никаких соображений на этот счёт не осталось.
В этот то момент мы и встали на скользкую дорожку. Ну самое очевидное — пользователи ведь жалуются не на то, что размер файла не определяется, а на то, что полоска загрузки замерла, на файл то им плевать с высокой колокольни.
Искушение злом. Вместо оправдания
Да, ключ к решению проблемы лежал в плоскости «вернуть правильный заголовок» и всё станет как было, но здесь мы ничего не добились. Потраченного времени жаль, — тратить его на эту «не ошибку» мы не были готовы изначально, а когда поиски не приводят к результату, а приводят к ещё большей трате — так время жаль вдвойне. В итоге решили поискать решение в другой плоскости.
Мы примерно знали сколько времени занимает загрузка (замеряли), понятно что эта величина непостоянная, зависит от сети, но при типичном сценарии загрузка колебалась в районе двух минут. Соответственно нам нужно было написать экран загрузки который бы развлекал пользователей это время. На самом деле чуть больше — на всякий случай с запасом.
Реализация обмана. Вместо охоты на баг
К счастью в Silverlight задача кастомизации экрана загрузки — типичная, нацелена не на фейковые экраны, а на всякое украшательство, но так или иначе гуглится легко, а там уже кто какие цели преследует — кто украшательство, кто подделку полосы прогресса. Нужно добавить два параметра splashscreensource
и onsourcedownloadprogresschanged
:
Первый — это визуальное представление, xaml-файл (LoadScene.xaml
):
Макет полосы загрузки
Второй — это скрипт для обработки загрузки.
Изначально это была просто полоска. Не знаю как так получилось, но со временем полоска, предназначенная для того чтобы заполняться равномерно в течении двух минут, превратилась в две: одна чтобы показывать общий прогресс, вторая чтобы показывать загрузку «текущего» модуля:
Макет экрана загрузки
Откуда мы знаем какой модуль загружается и сколько времени это займёт? Да не откуда — это тоже подделка. Обычный массив со списком строк, которые якобы названия модулей. Названия выводятся вместо слова «загрузка».
В итоге пользователь видит что нижняя полоса — прогресс модуля, загружается достаточно быстро, вероятно это и был ожидаемый эффект: система очень быстро грузит отдельные модули, а долго загружается потому что модулей много.
Ниже представлена XAML-разметка второго варианта:
LoadScene.xaml
Здесь у нас четыре квадрата: два для верхней полосы прогресса (progressBarBackground
, progressBar
), два для нижней.
По одному квадрату progressBarBackground
и progressBarBackground2
— представляют пустую незаполненную полосу прогресса, и ещё по одному progressBar
и progressBar2
меняют свою ширину по мере «загрузки» и тем самым иллюстрирует движение полосы прогресса.
Также здесь несколько текстовых блоков для отображения числа в процентах и названия исполняемого модуля.
Собственно для реализации анимации прогресса нужно сделать изменение ширины у progressBar
и progressBar2
, ну и надписи периодически менять.
Для всего этого необходимо реализовать onSourceDownloadProgressChanged
, возвращаемся к aspx файлу:
На что тут можно обратить внимание, во-первых: на diff
— это фейковый список загружаемых модулей, а i
— это индекс текущего загружаемого модуля.
И во-вторых: на функцию onSourceDownloadProgressChanged
, при нормальном сценарии, — если размер файла приходит корректный, она вызывается с некоторой периодичностью и в её параметрах содержится какая доля файла уже загружена, соответственно мы можем использовать это для честной визуализации. Однако в нашем случае функция вызывается всего два раза: в самом начале, когда загружено 0, и в самом конце, когда загружено 100%.
Этот код:
var val = sender.findName("progressBar").Width / sender.findName("progressBarBackground").Width;
if (eventArgs.progress > val)
{
sender.findName("LoadingText").Text = Math.round((eventArgs.progress * 100)) + "%";
sender.findName("progressBar").Width = eventArgs.progress * sender.findName("progressBarBackground").Width;
if (eventArgs.progress >= 1 / 4 * (i + 1) || eventArgs.progress >= 0.98) {
sender.findName("LoadingText2").Text = "100%";
sender.findName("progressBar2").Width = sender.findName("progressBarBackground2").Width;
}
}
Написан на всякий случай, чтобы не было накладок если ошибка с определением размера файла пропадёт так же внезапно, как и возникла.
В этом случае код должен попытаться правдоподобно скоординировать фейковый и реальный прогресс. По крайней мере чтобы это не сильно бросалось в глаза и наш обман не вскрылся.
Такого не случилось, но ожидаю что при таком стечении обстоятельств полоска будет заполняться плавно, как в фейковом алгоритме, и потом рывками, когда реальный прогресс загрузки начнёт обгонять поддельный.
Полоса загрузки модулей тоже начнёт двигаться рывками из-за условия в строках 7 — 10. Суть его в том, что если мы загрузили 25% от общего размера, то мы не должны показывать что грузится первый модуль, а писать уже про второй — с первым заканчивать. Если общий прогресс превысил 50%, то и второй модуль надо перестать грузить, показать что он загружен на 100% и переходить дальше и т.д. из расчёта 25% на модуль, — четыре модуля покажем и хватит.
Ну и если общий прогресс приближается к 100%, то и загружаемый сейчас модуль тоже должен сделать вид что полностью загружен.
На один листинг выше, в 22 строке есть условие
if (id === 0)
Сделано для тех же целей, — на случай если функция начнёт вызываться корректно. Если проверку условия не сделать — то запустится множество циклов в setInterval
и полоска загрузки будет двигаться очень быстро, дойдёт до 100% и замрёт так на пару минут.
Думаю это отличает нашу поддельную полосу загрузки от большинства других подделок: мы предусмотрели корректировку относительно реального прогресса.
Теперь о самих интервалах. Их два.
Первый:
id = setInterval(function() {
var rel = sender.findName("progressBar").Width / sender.findName("progressBarBackground").Width;
rel += (Math.random() * 2 + 2) / 100;
if (rel <= 0.96) {
sender.findName("LoadingText").Text = Math.round((rel * 100)) + "%";
sender.findName("progressBar").Width = rel * sender.findName("progressBarBackground").Width;
}
}, 3500);
Раз в 3.5 секунды изменяет общую полосу прогресса на случайную величину от 2 до 4 процентов. Замирает на 96% и делает вид что осталось совсем чуть-чуть, но он замер на какой-то тяжёлой операции, после которой сразу 100% и приложение запущено. Обычно загрузка завершилась раньше чем он доходил до 96%.
Второй:
setInterval(function ()
{
var rel1 = sender.findName("progressBar").Width / sender.findName("progressBarBackground").Width;
var rel2 = sender.findName("progressBar2").Width / sender.findName("progressBarBackground2").Width;
rel2 += (Math.random() * 2 + 2) / 100;
if (rel1 >= 0.96) {
sender.findName("progressBar2").Width = sender.findName("progressBarBackground2").Width;
sender.findName("LoadingText2").Text = "100%";
}
else if (rel2 >= 1) {
sender.findName("progressBar2").Width = 0;
sender.findName("LoadingText2").Text = "0%";
i++;
} else {
sender.findName("LoadingText2").Text = Math.round((rel2 * 100)) + "%";
sender.findName("progressBar2").Width = rel2 * sender.findName("progressBarBackground2").Width;
}
sender.findName("MessageText").Text = diff[i];
}, 500);
Второй интервал управляет полосой загрузки модуля. Если основная полоса загрузки подвисла на 96%, то делаем вид что текущий модуль загружен на 100%, но к следующую модулю не переходим, даже если в списке ещё что-то есть. Так и остаётся.
В остальных ситуациях плавно доходим до 100%, увеличиваем i
на единицу — доставая из массива «следующий модуль», сбрасываем полосу прогресса загрузки модуля на 0, и всё сначала.
Загрузка «модуля» идёт в 7 раз быстрее «общей» загрузки, поэтому на всякий случай в массиве необходимо иметь 7 элементов, за границу массива не выйдет т.к. при достижении общего прогресса в 96% — мы перестаём инкрементировать переменную i
. Хотя сейчас мне это не кажется надёжным, лучше было бы ещё сделать дополнительную проверку на значение i
, ну да ладно.
Вот и вся реализация.
Заключение. Вместо покаяния
Таким образом мы дурим пользователя за его же деньги. И обмануть его не трудно! Он сам обманываться рад! И это не фигура речи, дословно не помню, но желание коллективного пользователя было сформулировано как-то так: «Сделайте хоть что-нибудь чтобы мы видели что приложение не зависло, и примерно представляли сколько ещё осталось ждать».
С этой точки зрения мы достигли того чего хотел пользователь, приложение даже грузилось быстрее чем обещала полоса прогресса, как правило уже на 70–80% загрузка завершилась — приятный бонус за Ваше ожидание. Ну и никто больше не перезагружал страницу полагая что она зависла. Даже если бы она зависла на 96%, вряд ли бы кто-то нажал F5, ведь остался последний рывок и загрузка может завершиться в любой момент.
Если Вы читаете это как пользователь, не удивляйтесь что иногда полоса загрузки действительно не то чем кажется. Но я полагаю что в глубине души Вы и сами это давным-давно поняли, и даже готовы с этим мириться, и более того готовы простить нас — тех, кто подделывает экран загрузки, потому что почти всегда это ложь во благо.
Если Вы читаете это как разработчик — знайте, подделать экран загрузки это нормально, а порой необходимо.