Я думаю, многие в детстве рисовали самодельные открытки для мам, сестер, бабушек. В школе так уж точно. Однако, после определенного возраста увлечение подобными вещами остается уделом очень маленького числа людей. Вряд ли вы хоть раз дарили что-то подобное своей девушке/жене. А ведь им наверняка понравится, психология и всё такое.
Естественно, мы, программисты, народ плечистый, нас не заманишь бумагой шелковистой, поэтому открытку делать я решил в виде мобильного приложения, а не клеить на коленке из картона и цветной бумаги. Благо, смартфоны сейчас даже у бездомных есть.
Дисклеймер
Я ни коим образом не претендую на качество программного кода, представленного в посте, на оригинальность идеи либо ее реализации.
Я не несу ответственности за ваши воспаленные от ужасного кода глаза и оскорбление чувств программистов.
Я понимаю, что большинство принятых мной решений при разработке были неидальными, и надеюсь, что данный пост убережет других разработчиков от аналогичных промахов
Идея и анализ задачи
Итак, для успешного, яркого и запоминающегося поздравления нужен, что? Правильно, Вау-эффект! Для этого следует сделать все незаметно для объекта поздравления, и добавить немного программистской магии. На мое счастье, в Android её хватает, как белой так и черной.
Представьте, вы написали и установили приложение в телефон к жертве виновнику всей затеи, а потом что? "Запусти, пожалуйста, вон то приложение" или "Дай телефон на секунду… Смотри!". Вау-эффекта не будет, даже не надейтесь.
Поэтому самое лучшее, что пришло мне в голову — предустановить приложение (хотел замаскировать, но не успел, да и не пришлось, в общем-то), а в момент Х запустить его удаленно, с помощью магии, удивив ничего не подозревающего маггла.
Начинку открытки составляют памятные фотографии и короткие поздравления в стиле "желаю оставаться красивой, умной, доброй, счастья, здоровья". Набор индивидуальный, мало ли что вашему поздравляемому нравится.
Уже на стадии проектирования я совершил свою первую и самую критическую ошибку. Я решил, что успею за короткий срок сделать красивую открытку с кучей анимаций, не имея никакого опыта работы с этой самой анимацией и её оптимизацией. Не скажу, что визуально получилось плохо, но и без той анимации, что я добавил, всё было бы превосходно, и проблем стало б меньше.
Инструментарий
Для работы нам понадобятся:
- Любая IDE для андроид-разработки
- Фото
жертвыи стоковые изображения для открытки - Google Firebase либо Google Cloud Firestore
- Утилита curl либо любое другое средство отправки POST-запросов
Реализация
Для создания анимаций я воспользовался библиотекой WowoViewPager. Не советую ее использовать для подобных проектов.
Библиотека позволяет создавать анимированные слайды, работающие через ViewPager. Перелистывание по умолчанию осуществляется свайпами. Можно полностью настроить движение любых view-элементов, скорость и тип анимации. Поддерживаются gif и svg анимации.
Основным минусом, на мой взгляд, является требование все элементы хранить в одном xml-файле. Библиотека, судя по всему, не рассчитана на большое количество слайдов (в оригинальном примере их всего максимум 4). В моем случае 21 слайд и 16 jpeg-фото вызвало "отжирание" более 200 Мб памяти.
Расписывать процесс создания анимации я не стану, поскольку у каждого программиста свои костыли и велосипеды. Хочу лишь упомянуть, что для сокрытия тулбара и панели уведомлений можно воспользоваться флагами
getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
В процессе работы с библиотекой анимирования меня поджидал несколько неожиданный трабл. Классическая формула центрирования по абсолютным координатам
(screenWidth-viewWidth)/2
не работала; опытным путем я выяснил, что нужный эффект дает абсолютно аналогичная формула:
screenWidth/2 - viewWidth/2
Однако, в одном исключительном случае, фотография центрировалась только по формуле
screenWidth/3.5 - viewWidth/3.5
При том, что разница в размерах составляла всего 3 пикселя по оси Y (!sic)
Именно так я себя чувствовал в тот момент. Мне неизвестно, с чем это связано — с особенностями координат в андроид, багами библиотеки, или моими корявыми руками. Может быть, в комментариях кто-то сталкивался с аналогичными проблемами.
После реализации самой открытки, настал черед магии.
Прежде всего, создадим новый проект в Firebase Console.
При создании проекта укажите название и страну.
Следующим шагом, нужно подключить Firebase к написанной открытке. На выбор есть два варианта: Realtime Database и Cloud Firestore. В этом конкретном случае разницы нет, со своей задачей оба сервиса справляются прекрасно. В чем глобальная разница — я не знаю. Я использовал Cloud Firestore. По ссылке есть официальный туториал.
1) Укажите зависимости Gradle на уровне проекта
dependencies {
...
classpath 'com.google.gms:google-services:3.2.0'
...
}
2) Укажите зависимости Gradle на уровне модуля app. Сразу учтем зависимость для получения push-уведомлений
compile 'com.google.firebase:firebase-core:11.8.0'
compile 'com.google.firebase:firebase-firestore:11.8.0'
compile 'com.google.firebase:firebase-messaging:11.8.0'
3) Создайте и добавьте в манифест сервис для получения токена Firebase и дальнейшей записи в Cloud Firestore. Токен будет нам нужен для удаленного запуска приложения. Замечу, что у меня нет функции удаления старого токена из базы, не успел реализовать. Если вам потребуется, пример есть выше по ссылке.
<application>
...
<service android:name=".FirebaseIdService">
<intent-filter>
<action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
</intent-filter>
</service>
...
</application>
public class FirebaseIdService extends FirebaseInstanceIdService
{
@Override
public void onTokenRefresh()
{
//получаем токен в случае его обновления
String refreshedToken = FirebaseInstanceId.getInstance().getToken();
Log.d("TOKEN REFRESH", refreshedToken);
//получаем экземпляр класса
FirebaseFirestore db = FirebaseFirestore.getInstance();
Map<String, Object> data = new HashMap<>();
//помещаем токен в hashmap
data.put("token", refreshedToken);
//devices - название документа в базе данных
db.collection("devices")
.add(data)
.addOnSuccessListener(new OnSuccessListener<DocumentReference>()
{
@Override
public void onSuccess(DocumentReference documentReference)
{
Log.d("FIREBASE", "Data added, id: " + documentReference.getId());
}
})
.addOnFailureListener(new OnFailureListener()
{
@Override
public void onFailure(@NonNull Exception e)
{
Log.d("FIREBASE", "Data adding failed, exception: n" + e);
}
});
}
}
4) Создаем и добавляем в манифест сервис для получения push-уведомлений от Firebase. Ранее мы уже добавили зависимость в Gradle.
Я не буду объяснять принципы создания и работы push-уведомлений в android. Для этого есть профильные сайты. Приведенный пример должен сработать в большинстве смартфонов с андроид 4.2.2 и выше.
<application>
...
<service android:name=".FirebaseMessageHandleService">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
</intent-filter>
</service>
...
</application>
public class FirebaseMessageHandleService extends FirebaseMessagingService
{
@Override
public void onMessageReceived(RemoteMessage remoteMessage)
{
super.onMessageReceived(remoteMessage);
{
//Создаем интент для запуска уведомления
Intent intent = new Intent(this, MainActivity.class);
/**
* если экземпляр данной Activity уже существует,
* то все Activity, находящиеся поверх нее разрушаются,
* и этот экземпляр становится вершиной стека.
* Также вызовется onNewIntent()
*/
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
/**
* Создаем интент для доступа к правам текущего приложения
* Если уже есть такой интент - стираем его
*/
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
//Получаем объект менеджера уведомлений
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
//Метод устарел, но я взял из другого проекта, и лень переписывать. Не продакшн ведь.
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this);
//Указываем параметры уведомления: иконку, заголовок, текст, интент для запуска.
notificationBuilder.setContentIntent(contentIntent);
notificationBuilder.setSmallIcon(R.drawable.ic_launcher_background);
notificationBuilder.setContentTitle("Вам открытка!");
notificationBuilder.setContentText("Нажмите на меня для открытия");
//Создаем объект класса Notification
Notification notification = notificationBuilder.build();
notification.defaults = Notification.DEFAULT_SOUND;
notificationManager.notify(1, notification);
/**
* Немного магии.
* Именно эта небольшая часть кода запускает активити
* при получении push-уведомления из Firebase
* Сработает даже при заблокированном экране
*/
Intent intent1 = new Intent(getApplicationContext(), MainActivity.class);
intent1.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent1);
}
}
}
Дописывал открытку я уже ранним утром 8 марта. Катастрофически не успевал, до будильника оставались считанные минуты, а я еще не проверял, как работает приложение в смартфоне девушки (у нас разные разрешения экрана). У меня был доступ по отпечатку пальца. При настройке отладки по USB потребовалась перезагрузка. После перезагрузки оказалось, что я не помню графический пароль. В ту секунду я почувствовал себя полным дебилом. В шаге от финиша я не мог проверить работу приложения (facepalm). Пришлось будить девушку и заставить ее разблокировать, благо, спросонья она ничего не соображала и ввела пароль на автомате.
Финальная стадия разработки. Осталось только подготовить запрос для отправки в Firebase для инициализации push-уведомления.
Важный момент:
Firebase позволяет отправлять уведомление двумя способами: простым и кастомизируемым.
Простой вариант подразумевает отправку только двух полей — заголовка и текста уведомления. Такой тип сообщения можно отправить прямо из консоли Firebase, однако уведомление будет получено только в том случае, если есть работающий процесс приложения. Он нам не подходит, поскольку приложение никак не должно проявлять себя до момента X.
Кастомизируемый вариант позволяет отправлять сообщения в json-формате объемом до 4Кб с любым содержанием. Такое сообщение придет, даже если сообщение очищено из стека ранее запущенных приложений. (Однако, если приложение и его сервисы были убиты каким-нибудь чистильщиком памяти, уведомление не придет до момента перезапуска приложения). Минус кастомизируемого способа в том, что его можно отправить только POST-запросом к серверам Firebase.
Для отправки запроса я воспользовался утилитой curl. Поскольку код писался из-под Linux Mint, мне достаточно было запустить терминал. Вы можете использовать любой другой инструмент, например Postman.
"Authorization: key=<ваш ключ>"
Ключ можно посмотреть в настройках проекта Firebase (нажать на шестеренку)
"Content-Type: application/json"
{
"to":"Firebase-токен устройства",
"data":{"любые поля":"любые значения"},
"priority":10 //по умолчанию
}
curl -X POST --header "Authorization: key=your_key" --Header "Content-Type: application/json" https://fcm.googleapis.com/fcm/send -d "{"to" : "firebase_token" , "data":{"name" : "value"} , "priority" : 10}"
Теперь остается только в нужный момент отправить запрос!
Девушке понравилось, вау-эффект был достигнут :)
Автор: crazytosser00