Всплывающие уведомления в приложениях NativeScript Angular 2

Всплывающие уведомления в приложениях NativeScript Angular 2

Сегодня мы познакомимся как создавать всплывающие уведомления (Toast notifications) в NativeScript для Android и iOS, построенных с Angular 2.

На картинке выше вы можете увидеть результат нашей задачи.

Создадим простое приложение с одной кнопкой, при нажатии которой будет показано всплывающее уведомление и на iOS и в Android. Эти нотификации очень удобны для показа информации без прерывания работы пользователя с приложением.

Создаём проект NativeScript

В консоли поочерёдно выполните следующие команды:

tns create ToastProject --ng
cd ToastProject
tns platform add ios
tns platform add android

Параметр —ng укажет скрипту на создание приложения Angular на TypeScript. Также не забывайте, что без Mac с установленным Xcode, у вас не получится собрать приложение для iOS.

Ранее для создания всплывающих уведомлений требовалась работа напрямую с Android API. Но сейчас можно упростить этот процесс с помощью плагина и вдобавок получить возможность работы этих нотификаций в iOS.

Выполните следующую команду в консоли:

tns plugin add nativescript-toast
Теперь приступим непосредственно к разработке приложения.

Создание логики приложения и разметки XML

Это приложение будет состоять только из одной страницы. Мы начнём с добавления кода на TypeScript и затем построим простенький UI (пользовательский интерфейс).

Откроем файл app/app.component.ts в любимом редакторе и добавим в него такой код:

import { Component } from "@angular/core";
import * as Toast from "nativescript-toast";

@Component({
selector: "my-app",
templateUrl: "app.component.html",
})
export class AppComponent {

public showToast(message: string) {
Toast.makeText(message).show();
}

}
Здесь мы импортируем плагин, установленный ранее. С помощью функции мы можем показать всплывающее уведомление и так как функция публичная, мы можем вызвать её из UI.

Откроем файл app/app.component.html и вставим следующие строки:





В этом UI нет панели с названием приложения. Единственная кнопка расположена в центре экрана, она вызывает метод showToast при нажатии.

Nic Raboy: Display Toast Notifications In A NativeScript Angular Application

Вышел NativeScript 2.5

Вышел NativeScript 2.5
Отладчик в Chrome DevTools

Сегодня вышла версия 2.5 фреймворка для мобильной разработки NativeScript.

В этой версии появились многие долгожданные функции. Подробнее о них ниже.

  • Поддержка WebPack 2.0 с плагином nativescript-dev-webpack
  • Поддержка ленивой загрузки Angular 2
  • Механизм для копирования дополнительных файлов проекта на устройство с плагином copy-webpack-plugin
  • Поддержка минификации кода с Uglify.js. Минифицировать код можно с помощью флага —uglify во время сборки проекта
  • Компиляция Ahead of Time (AoT). Этот вид компиляции включается при установке плагина nativescript-dev-webpack в приложениях NS + Angular 2
  • Интеграция с отладчиком из Chrome для JavaScript и TypeScript. Для android запуск стандартный: tns debug android Для ios пока с дополнительным флагом: tns debug ios --chrome К сожалению, в этом релизе не удалось реализовать все функции отладчика (типа профайлера), но в ближайших версиях функционал будет расширен
  • Улучшения в CLI. Кроме исправления ошибок, были добавлены новые функции, одна из них — при запуске команды tns run она запускается с флагом livesync — watch, что включает автообновление приложения на устройстве при редактировании кода
  • Улучшения в расширении к Visual Studio Code делают редактирование, отладку и установку приложения намного быстрее
  • Обновление компонентов Telerik UI for NativeScript. Добавлен компонент Шкала и обновлены некоторые другие. Подробно об этом.
  • Обновлено демонстрационное приложение для iOS и Android
  • Новый фреймворк для тестирования приложений NS с открытым исходным кодом.

И немного картинок. Отладка в VS Code:
Вышел NativeScript 2.5

Обновлённое демонстрационное приложение:
Вышел NativeScript 2.5

Обновление существующих приложений

После обновления версии NativeScript CLI, перейдите в консоли в папку с приложением и запустите команду:
tns update
Теперь обновим платформы. Для android:
tns platform remove android
tns platform add android
И для iOS:

tns platform remove ios
tns platform add ios

Обновим модули tns-core:
npm install tns-core-modules@latest --save

Магазин плагинов

Вышел NativeScript 2.5
Эта новость не относится напрямую к NS 2.5, но мы ранее не упоминали об этом. Итак, свершилось! У NativeScript появился официальный репозиторий плагинов — теперь нам не нужно бороздить интернет в поисках необходимого плагина. Все они находятся в одном месте, популярные плагины автоматически поднимаются вверх в поиске

NativeScript Blog

Подключаем Admob к мобильному приложению на NativeScript Angular 2

Подключаем Admob к мобильному приложению на NativeScript Angular 2

Если вдруг вы ещё не знакомы с Admob, то это один из лучших способов монетизации мобильных приложений с помощью рекламы. Вы будете получать доход не только от тех, кто кликает на рекламу, но и от её показов на экране приложения.

Сегодня мы увидим как подключать рекламу Admob в приложении NativeScript под Android и iOS, собранном с Angular 2.

Чтобы было более понятно, начнём с пустого приложения. Откройте окно Командной строки (в Windows) или Terminal (в Mac и Linux) и выполните поочерёдно следующие команды:


tns create AdmobProject --ng
cd AdmobProject
tns platform add android
tns platform add ios

Флаг —ng указывает на то, что мы собираемся создавать проект Angular 2 с TypeScript. Здесь я добавляю обе платформы — Android и iOS. Но если у вас нет Mac, вы не сможете собрать приложение для iOS.

В этом проекте нам потребуется плагин nativescript-admob, созданный Eddy Verbruggen. Установим его:

tns plugin add nativescript-admob

Так как мы используем TypeScript в проекте, нам нужно сконфигурировать описания типов, включенных в плагин Admob. Хоть это и необязательно, но очень полезно в работе. Отройте файл проекта references.d.ts и добавьте в него:


/// Needed for autocompletion and compilation.
///

Теперь у нас в руках будет такой функционал из IDE, как автодополнение и, конечно, многое другое.

Подключаем Admob к мобильному приложению на NativeScript Angular 2 Подключаем Admob к мобильному приложению на NativeScript Angular 2 Подключаем Admob к мобильному приложению на NativeScript Angular 2

Наша цель — получить такой же результат, как на картинках выше. Если вы не сделали этого раньше, то пора зарегистрироваться в Google Admob и создать там два приложения. Два — по одному для каждой платформы, под iOS и Android. Это позволит отслеживать статистику по каждой платформе отдельно. Проверьте, что вы создали и модуль банера и модуль полноэкранной рекламы для каждого приложения.

Разработка логики на TypeScript

Откройте файл app/app.component.ts и добавьте в него такой TypeScript код:


import { Component } from "@angular/core";
import * as Admob from "nativescript-admob";

@Component({
selector: "my-app",
templateUrl: "app.component.html",
})
export class AppComponent {

private androidBannerId: string = "ca-app-pub-XXXX/YYYY";
private androidInterstitialId: string = "ca-app-pub-KKKK/LLLL";
private iosBannerId: string = "ca-app-pub-RRRR/TTTT";
private iosInterstitialId: string = "ca-app-pub-GGGG/HHHH";

public constructor() { }

public createBanner() {
Admob.createBanner({
testing: true,
size: Admob.AD_SIZE.SMART_BANNER,
iosBannerId: this.iosBannerId,
androidBannerId: this.androidBannerId,
iosTestDeviceIds: ["yourTestDeviceUDIDs"],
margins: {
bottom: 0
}
}).then(function() {
console.log("admob createBanner done");
}, function(error) {
console.log("admob createBanner error: " + error);
});
}

public hideBanner() {
Admob.hideBanner().then(function() {
console.log("admob hideBanner done");
}, function(error) {
console.log("admob hideBanner error: " + error);
});
}

public createInterstitial() {
Admob.createInterstitial({
testing: true,
iosInterstitialId: this.iosInterstitialId,
androidInterstitialId: this.androidInterstitialId,
iosTestDeviceIds: ["yourTestDeviceUDIDs"]
}).then(function() {
console.log("admob createInterstitial done");
}, function(error) {
console.log("admob createInterstitial error: " + error);
});
}

}

Не пугайтесь от объёма кода выше, ниже мы разберём его по кусочкам:

import { Component } from "@angular/core";
import * as Admob from "nativescript-admob";

Здесь мы импортируем такие зависимости, как Angular 2 и Admob, установленные ранее.

Внутри класса AppComponent есть следующие переменные:


private androidBannerId: string = "ca-app-pub-XXXX/YYYY";
private androidInterstitialId: string = "ca-app-pub-KKKK/LLLL";
private iosBannerId: string = "ca-app-pub-RRRR/TTTT";
private iosInterstitialId: string = "ca-app-pub-GGGG/HHHH";

Эти четыре переменные должны содержать id рекламных модулей, созданных вами в панели управления Admob. У меня в примере это просто случайные похожие значения, чтобы вы примерно представляли, о чём идёт речь.

У нас есть три различных метода для работы с рекламой, начнём с метода createBanner:


public createBanner() {
Admob.createBanner({
testing: true,
size: Admob.AD_SIZE.SMART_BANNER,
iosBannerId: this.iosBannerId,
androidBannerId: this.androidBannerId,
iosTestDeviceIds: ["yourTestDeviceUDIDs"],
margins: {
bottom: 0
}
}).then(function() {
console.log("admob createBanner done");
}, function(error) {
console.log("admob createBanner error: " + error);
});
}

Используя плагин NativeScript Admob, мы можем создать SMART_BANNER внизу экрана, используя id рекламных модулей или тестовые id. Также обратите внимание, что банер будет запущен в тестовом режиме — testing: true. Это оградит нас от загрузки настоящих рекламных банеров из Admob во время разработки и отладки приложения.

Остановимся немного на типах банеров. Их не так много:

  • SMART_BANNER
  • LARGE_BANNER
  • BANNER
  • MEDIUM_RECTANGLE
  • FULL_BANNER
  • LEADERBOARD
  • SKYSCRAPER
  • FLUID

Оставлю вам самим разобраться с тем, для чего используется каждый тип банера.

Кстати, вполне возможно, что вам нужно будет скрывать банер. Возможно, у вас в приложении будут встроенные покупки, удаляющие рекламу, и вы захотите скрывать её после оплаты. В этом-то случае и пригодится наш метод hideBanner:


public hideBanner() {
Admob.hideBanner().then(function() {
console.log("admob hideBanner done");
}, function(error) {
console.log("admob hideBanner error: " + error);
});
}

В этом коде только скрывается рекламный блок, ничего более.

Наконец, у нас ещё есть полноэкранная реклама. Если её использовать с умом, она принесёт наибольший доход от приложения. В методе createInterstitial мы делаем следующее:


public createInterstitial() {
Admob.createInterstitial({
testing: true,
iosInterstitialId: this.iosInterstitialId,
androidInterstitialId: this.androidInterstitialId,
iosTestDeviceIds: ["yourTestDeviceUDIDs"]
}).then(function() {
console.log("admob createInterstitial done");
}, function(error) {
console.log("admob createInterstitial error: " + error);
});
}

Также здесь мы используем тестовый режим с id, указанными в начале TypeScript-файла. Разница только в том, что мы используем Admob.createInterstitial вместо создания банера. Полноэкранная реклама выглядит как всплывающее окно, только занимая весь экран.

Теперь с TypeScript создадим простой, но мощный интерфейс.

Создание пользовательского интерфейса

Наш интерфейс будет содержать только три кнопки, каждая из которых при нажатии будет вызывать соответствующий метод из созданных нами ранее.

Откройте файл app/app.component.html и включите в него следующую HTML-разметку:








Как вы видите, здесь у нас есть верхняя панель и три кнопки. Каждая из кнопок стилизована в соответствии с включенной в NativeScript темой. При нажатии на кнопки будет вызван соответствующий ей TypeScript-метод для показа/скрытия банерной рекламы или показа полноэкранного модуля.

Заключение

Теперь вы знаете как подключить Google Admob в проект NativeScript Angular 2. Рекламные модули очень хороши для получения заработка от мобильных приложений без взимания каких-либо оплат с пользователей. Не забудьте выключить режим testing перед выкладыванием приложения в магазин, иначе вы ничего не заработаете 🙂

Nic Raboy: Using Google Admob In Your NativeScript Angular 2 Mobile App

Минификация JavaScript кода в приложении NativeScript Angular 2

Минификация JavaScript кода в приложении NativeScript Angular 2

Сегодня я покажу вам как использовать WebPack, Babel и UglifyJS для минификации, искажения и склеивания JavaScript в приложениях NativeScript Angular 2.

Устанавливаем NativeScript:

npm install nativescript -g

Создаём приложение Angular 2:

tns create MyApp --ng

Устанавливаем nativescript-dev-webpack, babel-core, babel-preset-es2015 и babel-loader в наше приложение:

cd MyApp
npm install nativescript-dev-webpack --save-dev
npm install babel-core --save-dev
npm install babel-preset-es2015 --save-dev
npm install babel-loader --save-dev

Открываем приложение в любимом редакторе и добавляем babel-loader в конец секции загрузчиков и плагин UglifyJS в конец секции плагинов в webpack.common.js:

{
test: /nativescript-intl.*\.js$/,
loader: 'babel-loader',
query: {
presets: ['es2015']
}
}

Минификация JavaScript кода в приложении NativeScript Angular 2

new webpack.optimize.UglifyJsPlugin({ compress: false })

Минификация JavaScript кода в приложении NativeScript Angular 2

* Сжатие отключено из-за генерации классов Android в NativeScript во время сборки. Компрессор UglifyJS это преобразователь синтаксического дерева, уменьшающий размер кода, делая различные оптимизации с деревом.

Создаём бандл и запускаем приложение:

npm run start-android-bundle

или так для ios:

npm run start-ios-bundle

Результат можно увидеть в папке платформы ios/android:
Минификация JavaScript кода в приложении NativeScript Angular 2

Созданный JavaScript код минифицирован, искажён и забандлен.
Наслаждайтесь!

Источник: Minify, uglify and bundle your JavaScript in NativeScript Angular 2 app

Создание кроссплатформенного плеера для SoundCloud® с Fuse

Создание кроссплатформенного плеера для SoundCloud® с Fuse

Мы постоянно получаем запросы от наших пользователей, которые хотят увидеть, как выглядят “реальные программы”, сделанные с Fuse. Наш учебник специально предназначен для быстрого начала работы с Fuse, но при этом он пока не содержит описаний сложных задач.

Поэтому я решил сделать полную программу, с публикацией в магазины приложений. Она будет служить примером по-настоящему сложных задач и будет великолепным мастер-классом.

Приложение будет работать с реальным бекендом, оно будет кроссплатформенным (работать в Android и iOS) и будет использовать несколько интересных нативных интеграций на обеих платформах.

Как мы видим, SoundCloud® имеет всё необходимое:

  • Они предоставляют REST API (и он бесплатен)
  • Есть много контента для работы (картинки и музыка)
  • Нашему приложению понадобятся нативные компоненты (контролы для управления музыкой и т.п.)

Перед тем, как начать…

Вы можете не устанавливать Fuse и не читать код на Github, чтобы увидеть готовый результат.

Внимание: приложение FuseCloud это неофициальный плеер для SoundCloud, и никаким образом не связано с SoundCloud. Оно просто использует SoundCloud API.

Реализация

Есть три главных подзадачи: пользовательский интерфейс, написанный на UX и JavaScript; обёртка вокруг SoundCloud REST API; нативный музыкальный плеер.

Навигация

Я использовал компоненты Router и Navigator для построения большей части навигации, с единственным исключением для PageControl в главном представлении, в котором вы можете переключаться между тремя табами (лента новостей, поиск, избранное). После создания каждой страницы приложения отдельным компонентом, навигация будет примерно такой:

// fusecloudnavigationstructure.ux
<Navigator DefaultTemplate="main">
    
    <FuseCloud.MainPage ux:Name="main">
        <PageControl ux:Name="pageControl" Active="searchPage">
            <Page ux:Name="newsFeedPage" />
            <Page ux:Name="searchPage" />
            <Page ux:Name="favoritesPage" />
        </PageControl>
    </FuseCloud.MainPage>
    
    <FuseCloud.CommentsPage ux:Template="comments" router="router" />
    
    <FuseCloud.TrackDetailsPage ux:Name="track" router="router"/>
    
</Navigator>
Бесконечная прокрутка

Благодаря новым возможностям Fuse, я легко создал плавную бесконечную прокрутку для отображения всех комментариев к каждой композиции. Вставляя отдельные комментарии в блок Deferred, я защищён от глюков при автоматической подгрузке новых элементов. Ниже вы можете увидеть пример UX-кода для создания такой прокрутки:

// fusecloudendlessscroller.ux
<ScrollView ClipToBounds="False">
    <StackPanel>
        <Each Items="{comments}">
            <Deferred>
                <FuseCloud.DividerLine Alignment="Top"/>
                <FuseCloud.Comment ux:Name="comment" ThumbnailUrl="{avatar_url}" Username="{username}" Body="{body}" />
            </Deferred>
        </Each>
    </StackPanel>
    <Scrolled To="End" Within="100">
        <Callback Handler="{showMoreComments}" />
    </Scrolled>
</ScrollView>

Прокрутка на 100 точек от нижнего края приложения вызывает JavaScript-функцию, которая подгружает следующие комментарии:

// fusecloudendlessscroller.js
function showMoreComments() {
    if (nCommentsShowing < allComments.length) {
        nCommentsShowing += nCommentsPerPage;
        while (comments.length < nCommentsShowing && comments.length < allComments.length - 1) {
            comments.add(allComments.getAt(comments.length));
        }
    }
}

Замутнение фона

Экран проигрывателя показывает изображение альбома текущей дорожки посередине страницы, при этом заполняя весь фон затемнённой копией этого же изображения. В Fuse такое делается одной строчкой кода: , но замутнение больших элементов может плохо отразиться на производительности. Поэтому я использую классический трюк с программной GPU-обработкой для улучшения скорости:

// fusecloudscaledblur.ux
<FuseCloud.AlbumArt Width="20%" Height="20%">
    <Blur Radius="2"/>
    <Scaling Factor="5" />
</FuseCloud.AlbumArt>

 

Здесь я уменьшаю размер изображения до 20% от оригинального и замутняю уменьшенное изображение, затем увеличиваю получившееся изображение до нормального размера.

Создание кроссплатформенного плеера для SoundCloud® с Fuse

Работаем с SoundCloud API

Вообще это несложная задача и в ней нет никаких специфичных для Fuse техник. Я структурировал обёртку, поэтому каждый запрос возвращает promise. Модель приложения была использована как интерфейс к API через набор функций-геттеров, возвращавших promise-ы в Observable. Код ниже наглядно иллюстрирует этот подход:

Функция, используемая для получения статуса лайка для трека, возвращает promise:

// fusecloudislikingtrackfetch.js
function isLikingTrack(trackId) {
    return Auth.getAccessToken()
        .then(function(token) {
            return FuseCloudGet("me/favorites/" + trackId, {}, token);
        });
}

 

Модель превращает этот promise в observable, используя удобную функцию (DelayedObservable):

// fusecloudislikingtrackobservable.js
function GetIsLikingTrack(trackId) {
    return DelayedObservable(function(obs) {
        FuseCloud.isLikingTrack(trackId)
            .then(function(result) {
                obs.add(result);
            });
    });
}

 

Это реально удобно, учитывая что возвращённый observable будет заполнен сразу при получении данных. Затем мы можем сделать привязку к нему и не беспокоиться об обратных вызовах или обновлениях интерфейса приложения.

Функция DelayedObservable работает как мост между API, основанной на promise-ах, и API, основанной на Observable:

// fuseclouddelayedobservable.js
function DelayedObservable(getter) {
    var ret = Observable();
    getter(ret);
    return ret;
}

 

Эта функция отвечает за обновление Observable при загрузке данных.

OAuth 2.0

SoundCloud API позволяет авторизоваться с помощью протокола OAuth 2.0. Используя модуль InterApp, я легко перекидываю пользователя на авторизацию с помощью нативного браузера:

// fusecloudlaunchuri.js
var uri = "https://soundcloud.com/connect?client_id=" + clientId
        + "&display=popup"
        + "&response_type=code"
        + "&redirect_uri=fuse-soundcloud://fuse";
InterApp.launchUri(uri);

 

В URL выше передаётся URI обратного вызова, который SoundCloud использует для возврата токена. Fuse позволяет зарегистрировать свою URI-схему в файле проекта:

// fusecloudcustomurischeme.json
"Mobile":{
    "UriScheme": "fuse-soundcloud"
}

 

Таким образом, SoundCloud API автоматически вернётся в нашу программу, как только токен доступа будет готов.

Создание кроссплатформенного аудиоплеера

Реализация обёртки для нативных плееров была наиболее интересной частью процесса. Частично из-за того, что API для этого различны у Android и iOS, но также по причине монолитной природы медиа-плееров. Я начал с минимального набора требований.

Наш StreamingPlayer должен:

  • Транслировать аудио по URL
  • Продолжать играть, когда приложение уходит в фон
  • Позволять переключаться между треками, пока приложение находится в фоне (используя нативные контролы на экране блокировки)
  • Отображать обложку альбома на экране блокировки

Создание кроссплатформенного плеера для SoundCloud® с Fuse

На бумаге это не кажется слишком сложной задачей, но на деле она оказалась настоящим вызовом.

Прежде всего, подключение к нативному аудиоплееру для проигрывания URL было суперпростым. API у Android MediaPlayer-а и AVPlayer у iOS предлагают это прямо из коробки. Моей начальной задумкой было использовать минимальную обёртку вокруг обоих этих API и просто делать остальную работу (типа управления плейлистами и состоянием) в JavaScript. Но ограничение на фоновое выполнение JS на этих платформах поставило крест на этом (одно из наших требований — возможность использовать контролы на экране блокировки).

Это означало, что я должен реализовать работу с плейлистами в нативном коде, при этом учитывая особенности Android и iOS. К счастью, всё оказалось гораздо проще, так как возможности внешнего кода Fuse позволяют вам легко интегрировать код на Java и Objective-C в проекты Fuse. Это очень удобно!

Другой интересной задачей было получение текущего состояния плеера для обоих компонентов, MediaPlayer и AVPlayer. Оба этих API имеют разные модели состояния и разные пути управления ими, но я нашёл универсальный способ.

И, наконец, работа с экраном блокировки. В iOS это крайне просто; достаточно зарегистрировать несколько системных вызовов. В Android же это не такая простая задача. В API, начиная с уровня 21, Android может получать медиа-нотификации, которые замещают обычные контролы на экране блокировки. Но нужно копать в сторону системы intent-ов для настройки коммуникации между нотификациями и фоновой службой.

Возможности

В приложение FuseCloud встроены очень большие возможности и механизмы, и так как я люблю перечисления, вот небольшой список фич, заложенных в этой программе (и исходном коде):

  • Аутентификация в SoundCloud® по протоколу OAuth 2.0
  • Использование пакета InterApp для запуска url во внешнем браузере и передача отклика по URI
  • Автоматическое обновление некорректных токенов
  • Получение данных по REST API
  • Лента новостей, поиск треков, избранное
  • Возможность поставить лайк и дизлайк треку
  • Обложки треков
  • Отображение комментариев к треку
  • Размещение комментариев
  • Статистика пользователя
  • Смахивание влево/вправо для переключения дорожки
  • Потягивание экрана для обновления
  • Бесконечный список прокрутки
  • Смахивание для показа действий с элементом (дизлайк в избранном)
  • Сохранение состояния UI с использованием Storage API (приветственная информация показывается только один раз при начале работы с программой)®
  • HTTP Audio StreamingPlayer для iOS и Android
  • Трансляция музыки из SoundCloud®
  • Настраиваемая панель перемотки
  • Фоновое проигрывание
  • Контролы на экране блокировки в iOS и Android
  • iOS: следующий, предыдущий, играть/пауза, перемотка на экране блокировки
  • Обложка альбома на экране блокировки
  • Нотификации в Android: следующий, предыдущий, играть/пауза
  • Показ обложки альбома в нотификации и в фоне
  • Плейлисты
  • Автопроигрывание следующего при окончании трека

Выводы и загрузки

Было реально классно работать над этим проектом. Я необъективен, но Fuse реально впечатлила меня  — в который раз.

И механизмы внешнего кода Fuse оказались действительно хорошим способом создания нативных компонентов. Они позволяют использовать документацию к API каждой платформы на своём языке, где это возможно, и без обёрток на JavaScript.

Вы можете скачать приложение FuseCloud для Android и iOS в Apple App Store, в Google Play, и исходный код на Github.

Внимание: ещё раз о создании “реальной” программы (с нативными компонентами и интеграциями с бекендом) — вы почти наверняка столкнетесь с некоторыми трудностями. Мы постоянно улучшаем нашу документацию, но если всё-таки встретите такой случай, дайте знать об этом нам и сообществу, и мы с радостью вам поможем 🙂

Узнать больше о Fuse можно посмотрев постоянно растущий список примеров (с исходным кодом, конечно), вступайте в наше сообщество (у нас есть классный форум и группа в Slack) или подписывайтесь на нас в Twitter или Facebook.

Автор оригинала Kristian Hasselknippe, Software Engineer at Fuse

Создание анимированной кнопки «Поделиться» в NativeScript + Angular

Создание анимированной кнопки "Поделиться" в NativeScript + Angular

Сегодня я покажу вам как создавать анимированную кнопку «Поделиться» в NativeScript и Angular. При нажатии этой кнопки будут показаны маленькие кнопки соцсетей по кругу от главной.

Исходный код примера вы можете увидеть на Github.

Итак, приступим!

Установка

Создадим проект, используя параметр —ng для создания приложения angular:

tns create --ng tns-animated-social-button

В нашем приложении будет использоваться плагин ng2-fonticon от Nathan Walker для вывода иконок на кнопках. Установите его по инструкции на этой странице.
Также мы используем пакет lodash. Установим его:

npm install --save lodash
npm install --save @types/lodash

Теперь приступим к нашему коду.

Создание SocialShareButtonComponent

Ядро нашего приложения будет описано в компоненте SocialShareButtonComponent. В шаблоне будут главная кнопка и несколько кнопок социальных сетей.
При нажатии на главную кнопку выезжают маленькие кнопки, а при повторном нажатии они возвращаются обратно. Для кнопок мы используем иконку «круг» из font awesome. Иконочные шрифты очень хороши тем, что они одинаково выглядят на любом экране и разрешении. При этом нужно помнить, что их размер контролируется параметром font-size. Для того, чтобы сделать необходимый размер компонента, мы должны выполнить некоторые расчёты — это из-за того, что не все иконки в шрифте имеют одинаковый размер.
На вход мы будем принимать массив наименований для иконок. И использовать его для создания соответствующих кнопок. Наименования возьмём из списка иконок font awesome. Теперь, зная всё это, давайте создадим компонент в новой папке social-share-button:

// app/social-share-button/social-share-button.component.html

import {
Component,
Input
} from '@angular/core';
import { TNSFontIconModule } from 'nativescript-ng2-fonticon';

@Component({
selector: 'social-share-button',
templateUrl: 'social-share-button/social-share-button.component.html',
styleUrls: ['social-share-button/social-share-button.component.css']
})
export class SocialShareButtonComponent {
@Input('size') size = 75;
@Input('shareIcons') shareIcons: string[];

public get mainIconSize(): number {
return this.size * 0.45;
}

public get shareButtonSize(): number {
return this.size * 0.55;
}

public get shareIconSize(): number {
return this.shareButtonSize * 0.5;
}

public get viewHeight(): number {
return this.size + this.shareButtonSize * 1.2;
}

public get viewWidth(): number {
return this.size + this.shareButtonSize * 2.2;
}

constructor(private fonticon: TNSFontIconModule) {}
}

В переменной size мы будем хранить расчитанный размер под разные разрешения, установим по-умолчанию его в 75. Она будет отвечать за параметр font-size главной кнопки. Переменная mainIconSize это размер иконки в главной кнопке. Переменная shareButtonSize отвечает за размер других кнопок, а shareIconSize, за размер иконки в них. Свойства viewHeight и viewWidth отвечают за внешние размеры всего представления. Нам нужно достаточно места для отображения главной кнопки, а также всех остальных малых кнопок. У нас будет максимум одна кнопка рядом с главной, поэтому высота никогда не превысит size + shareButtonSize. Что касается ширины, у нас будет по одной кнопке с каждой стороны, а в итоге: size + shareButtonSize x 2. Мы используем коэффициенты в том числе для того, чтобы было немного дополнительного пространства.

Создадим такой шаблон:












Кнопки помещаются в GridLayout таким образом, чтобы иконки находились поверх кругов. Всё содержимое в свою очередь, помещается в GridLayout, к которому мы динамически применили такие свойства, как высота и ширина.
Для создания кнопок соцсетей мы проходим в цикле по массиву переданных иконок. Текстом иконки будет конкатенация ‘fa-‘ и значения shareIcon.
Затем создадим соответствующую таблицу стилей:

/* app/social-share-button/social-share-button.component.css */

GridLayout {
text-align: center;
vertical-align: center;
}

Label.button {
color: #000;
}

Label.share-icon {
color: #FFF;
vertical-align: center;
}

Здесь мы всего лишь удостоверимся, что всё содержимое GridLayout отцентрировано и зададим кое-какие цвета. Также сделаем, чтобы иконки были отцентрированы по вертикали внутри кнопки.

Перед тем, как перейти к реализации, выведем результат в AppComponent. Сначала добавим Component в список деклараций AppModule:

// app/app.module.ts

import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core";
import { NativeScriptModule } from "nativescript-angular/platform";
import { SocialShareButtonComponent } from './social-share-button/social-share-button.component';
import { TNSFontIconModule } from 'nativescript-ng2-fonticon';
import { AppComponent } from "./app.component";

@NgModule({
declarations: [
AppComponent,
SocialShareButtonComponent
],
bootstrap: [AppComponent],
imports: [
NativeScriptModule,
TNSFontIconModule.forRoot({
'fa': 'font-awesome.css'
})
],
schemas: [NO_ERRORS_SCHEMA]
})
export class AppModule { }

Затем откроем AppComponent и немного причешем код:

// app/app.component.ts

import { Component } from "@angular/core";

@Component({
selector: "my-app",
templateUrl: "app.component.html",
styleUrls: ['app.component.css']
})
export class AppComponent {
}

Создайте шаблон app.component.html и вставьте в него следующее:



И файл CSS:

/* app/app.component.css */

StackLayout.container {
width: 100%;
vertical-align: center;
margin-left: auto;
margin-right: auto;
}

В результате должно получиться такое:
Создание анимированной кнопки "Поделиться" в NativeScript + Angular

Анимация кнопок

Сейчас мы поработает над анимациями вокруг главной кнопки. Сперва создадим свойство @ViewChildren() для получения GridLayout-ов всех кнопок:


@ViewChildren('shareButton') shareButtonRefs: QueryList;

private get shareButtons(): Array {
return this.shareButtonRefs.map(s => s.nativeElement);
}

Мы хотим сделать двухэтапные анимации. Сначала кнопки соцсетей вылетают из-за главной кнопки простым линейным перемещением. Затем нам нужно сделать кое-что посложнее — нам нужно, чтобы мелкие кнопки вылетали по кругу от главной. Финальная позиция кнопки в круге будет зависеть от положения других кнопок или, другими словами, от её позиции в массиве shareButtons.
Для создания кругового перемещения вспомним, как расчитываются координаты x, y от точки по краю окружности, в угловой функции:

Создание анимированной кнопки "Поделиться" в NativeScript + Angular
где x0 и y0 это координаты начала круга, r это его радиус, а ϴ это угол.

Чтобы мы могли увидеть круговую анимацию, нельзя просто переместить точку по кругу. Это бы привело к пересечению окружности:
Создание анимированной кнопки "Поделиться" в NativeScript + Angular
Вместо этого нужно сделать несколько последовательных перемещений, малыми шагами (маленькие вариации ϴ):
Создание анимированной кнопки "Поделиться" в NativeScript + Angular
Переведём теперь это в код.

Анимируем малые кнопки вокруг главной

Отследим тап по главной кнопке методом onMainButtonTap() нашего Component:

[...]

[...]

И соответствующий метод в Component:

// app/social-share-button/social-share-button.component.ts

[...]
import { Animation } from 'ui/animation';
[...]
constructor(private fonticon: TNSFontIconModule) {}

public onMainButtonTap(): void {
const animationDefinitions = this.shareButtons.map(button => {
return {
target: button,
translate: { x: this.size * 0.8, y: 0 },
duration: 200
};
});
const animation = new Animation(animationDefinitions);
animation.play();
}
}

Перемещение по оси x будет равно значению, прямо пропорциональному размеру главной кнопки. С этого значения и начнутся все вращения. Если вернуться к расчетам координат, то это будет радиусом, вокруг которого мы вращаем кнопки. Зная всё это, создадим свойство-getter для получения этого значения:


[...]
private get buttonRotationRadius(): number {
return this.size * 0.8;
}

[...]
public onMainButtonTap(): void {
const animationDefinitions = this.shareButtons.map(button => {
return {
target: button,
translate: { x: this.buttonRotationRadius, y: 0 },
duration: 200
};
});
const animation = new Animation(animationDefinitions);
animation.play();
}
}

Отсюда угол ϴ равен нулю. Теперь перейдём к самому забавному: вращению кнопок.

Круговые перемещения кнопок

Перед тем, как мы продолжим реализацию метода, давайте подумаем о том, что нам нужно. Мы говорили, что хотим иметь возможность перемещать кнопки на небольшие углы, или пошагово. Нам также нужно расчитать значение максимального угла перемещения каждой кнопки, в зависимости от её позиции в массиве. Сделаем пока только это и создадим два следующих метода:

[...]
import { range } from 'lodash';
[...]

private maxAngleFor(index: number): number {
return index * 45;
}

private angleIntervals(maxAngle: number): Array {
const step = 5;
return range(0, maxAngle + step, step);
}
}

Метод maxAngleFor() на входе принимает index и возвращает его, умноженным на 45. Это значит, что каждая кнопка будет отделена четвертью круга — для симметрии.
Метод angleIntervals() принимает maxAngle, и возвращает массив последовательных значений с шагом 5, в пределах maxAngle. Это будут наши шаги вращения.
Также мы реализуем метод получения координат точки, соответствующей значению угла:

[...]
import { Animation, Pair } from 'ui/animation';
[...]
private buttonCoordinatesFor(angle: number): Pair {
const x = this.buttonRotationRadius * Math.cos(angle * Math.PI / 180);
const y = this.buttonRotationRadius * Math.sin(angle * Math.PI / 180);

return { x: x, y: y };
}
}

Теперь важная задача — сделать перемещения кнопок с привязкой к шагу по окружности. Одно из решений для этого — создать массив из AnimationDefinition, как мы сделали в предыдущем разделе, и вызывать анимации с флагом playSequentially. К сожалению, сделай мы так, это привело бы к очистке представления после каждого шага анимации, что нам абсолютно не нужно. Другое решение — к каждому шагу анимации привязывать возвращённое значение Promise через метод then(). Мы можем сделать это с помощью метода reduce(), вызванного после метода angleIntervals(). Несколько строк кода расскажут нам больше тысячи слов:


[...]
animation.play().then(() => {
this.shareButtons.forEach((button, index) => {
const maxAngle = this.maxAngleFor(index);
this.angleIntervals(maxAngle).reduce((accumulator, currentAngle, index) => {
return accumulator.then(() => {
return button.animate({
translate: this.buttonCoordinatesFor(currentAngle),
duration: 0.8
});
});
}, Promise.resolve({}));
});
[...]

Для каждой кнопки мы получаем соответствующее ей значение maxAngle. И используем его для расчёта угловых шагов, вызывая метод reduce, связывая вместе все Promise (мы начали с результата пустого Promise). Продолжительность анимации занимает всего 0.8 мс, так мы перемещаем кнопку на соответствующие координаты для текущего угла. Напомню, что начинаем мы с угла, равного 0.

После небольшого рефакторинга, это превращается в:


[...]
public onMainButtonTap(): void {
this.translateShareButtonsOutOfMainButton().then(() => {
this.rotateShareButtonsAroundMainButton();
});
}

private translateShareButtonsOutOfMainButton(): AnimationPromise {
const animationDefinitions = this.shareButtons.map(button => {
return {
target: button,
translate: { x: this.circularRotationRadius, y: 0 },
duration: 200
};
});
const animation = new Animation(animationDefinitions);
return animation.play();
}

private rotateShareButtonsAroundMainButton(): void {
this.shareButtons.forEach((button, index) => {
this.rotateAroundMainButton(button, index);
});
}

private rotateAroundMainButton(button: GridLayout, index: number): AnimationPromise {
const maxAngle = this.maxAngleFor(index);
return this.angleIntervals(maxAngle).reduce(
this.getStepRotationAccumulatorFor(button),Promise.resolve()
);
}

private getStepRotationAccumulatorFor(button: GridLayout) {
return (accumulator, currentAngle, index) => {
return accumulator.then(() => this.doStepRotation(button, currentAngle));
}
}

private doStepRotation(button: GridLayout, angle: number): AnimationPromise {
return button.animate({
translate: this.buttonCoordinatesFor(angle),
duration: 0.8
});
}
}

Возврат кнопок на место

Когда кнопки покажутся, нам понадобится способ вернуть их назад, откуда они вышли. Чтобы это сделать, нам понадобится флаг shareButtonDisplayed, показывающий видимость кнопок:


[...]
@Component({
selector: 'social-share-button',
templateUrl: 'social-share-button/social-share-button.component.html',
styleUrls: ['social-share-button/social-share-button.component.css']
})
export class SocialShareButtonComponent {

private shareButtonDisplayed = false;
[...]

Анимация обратного возврата кнопок будет очень похожа на translateShareButtonsOutOfMainButton(), поэтому мы возьмём содержимое метода, чтобы сделать его более унифицированным:


[...]
private translateShareButtonsOutOfMainButton(): AnimationPromise {
return this.translateShareButtonsTo({
x: this.circularRotationRadius,
y: 0
})
}

private translateShareButtonsTo(coordinates: Pair): AnimationPromise {
const animationDefinitions = this.shareButtons.map(button => {
return {
target: button,
translate: coordinates,
duration: 200
};
});
const animation = new Animation(animationDefinitions);
return animation.play();
}
[...]

Что позволит нам написать:


[...]
private translateShareButtonsBackInMainButton(): AnimationPromise {
return this.translateShareButtonsTo({ x: 0, y: 0 });
}
[...]

И теперь мы можем переписать onMainButtonTap():


[...]
public onMainButtonTap(): void {
if (!this.shareButtonDisplayed) {
this.translateShareButtonsOutOfMainButton().then(() => {
this.rotateShareButtonsAroundMainButton();
});
}
else {
this.translateShareButtonsBackInMainButton();
}
this.shareButtonDisplayed = !this.shareButtonDisplayed;
}
[...]

Проблема текущей реализации в том, что пользователь может поломать нашу анимацию. Чтобы этого избежать, мы введём переменную-перечисление State, показывающую состояние Component: ожидание, проигрывание или остановлен. Перед этим необходимо переделать метод rotateShareButtonsAroundMainButton() для возврата Promise. В этом методе мы хотим возвращать результат Promise-ов всех анимаций, поэтому мы должны поймать момент окончания всей анимации (остановлен). Изменим метод следующим образом:


[...]
private rotateShareButtonsAroundMainButton(): AnimationPromise {
const animationPromises = this.shareButtons.map((button, index) => {
return this.rotateAroundMainButton(button, index);
});
return Promise.all(animationPromises);
}
[...]

Изменим флаг по состоянию анимации:

[...]
enum AnimationState {
idle,
animating,
settled
}

@Component({
selector: 'social-share-button',
templateUrl: 'social-share-button/social-share-button.component.html',
styleUrls: ['social-share-button/social-share-button.component.css']
})
export class SocialShareButtonComponent {

private animationState = AnimationState.idle;
[...]

И финальная реализация:


[...]
public onMainButtonTap(): void {
if (this.animationState === AnimationState.idle) {
this.translateShareButtonsOutOfMainButton().then(() => {
this.animationState = AnimationState.animating;
return this.rotateShareButtonsAroundMainButton();
}).then(() => {
this.animationState = AnimationState.settled;
});
}
if (this.animationState === AnimationState.settled) {
this.translateShareButtonsBackInMainButton().then(() => {
this.animationState = AnimationState.idle;
});
}
}
[...]

К этому моменту вы уже должны убедиться в красоте Promise-ов в JavaScript.

Делаем кнопки настраиваемыми

Сейчас наши черно-белые кнопки выглядят очень скучно. Сделаем их настраиваемыми. Добавим пару Input-ов (с некоторыми значениями по-умолчанию):


[...]
@Input('buttonColor') buttonColor = '#CC0000';
@Input('iconColor') iconColor = '#FFFFFF';
[...]

И привяжем к шаблону:












А также можно немного подсократить таблицу стилей:


/* app/social-share-button/social-share-button.component.css */

GridLayout {
text-align: center;
vertical-align: center;
}

Label.share-icon {
vertical-align: center;
}

Добавим эффект тени с помощью нативного кода

Немного улучшим стиль кнопки, добавив к ней тень. NativeScript пока не поддерживает показ тени в представлении, поэтому мы сделаем это на нативном коде, с помощью Directive, которая может быть реализована и для iOS и для Android.
Создадим новую папку специально для кода нашей Directive, назовём её label-shadow. Теперь создадим абстрактную базовую директиву, которая будет унаследована каждой платформой:


// app/label-shadow/label-shadow-base.directive.ts

import { Directive, ElementRef } from '@angular/core';
import { Label } from 'ui/label';
import { Observable } from 'data/observable';
import { Color } from 'color';

@Directive({
selector: '[shadow]'
})

export abstract class LabelShadowBaseDirective {

private get label(): Label {
return this.el.nativeElement;
}

protected get shadowColor(): Color {
return new Color('#888888');
}

protected get shadowOffset(): number {
return 5.0;
}

constructor(protected el: ElementRef) {
this.label.on(Observable.propertyChangeEvent, () => {
if (this.label.text !== undefined) {
this.displayShadowOn(this.label);
}
});
}

protected abstract displayShadowOn(label: Label);
}

Нам нужно подождать, пока Label с плагином FontIcon настроится, поэтому добавим хук — перехватчик события. По его готовности мы применим абстрактный метод displayShadowOn().

Перед тем, как взяться за реализацию, создадим описание типов, которое покажет TypeScript, что директива здесь будет во время компиляции:


// app/label-shadow/label-shadow.directive.ts

import { Label } from 'ui/label';
import { LabelShadowBaseDirective } from './label-shadow-base.directive';

export declare class LabelShadowDirective extends LabelShadowBaseDirective {
constructor(label: Label);
protected displayShadowOn(label: Label);
}

Создадим реализацию под Android:


// app/label/label-shadow.directive.android.ts

import { Directive, ElementRef } from '@angular/core';
import { Label } from 'ui/label';
import { LabelShadowBaseDirective } from './label-shadow-base.directive';
import { Color } from 'color';

@Directive({
selector: '[shadow]'
})
export class LabelShadowDirective extends LabelShadowBaseDirective {
constructor(protected el: ElementRef) {
super(el);
}

protected displayShadowOn(label: Label) {
const nativeView = label.android;
nativeView.setShadowLayer(
10.0,
this.shadowOffset,
this.shadowOffset,
this.shadowColor.android
);
}
}

И для iOS:


// app/label-shadow/label-shadow.directive.ios.ts

import { Directive, ElementRef } from '@angular/core';
import { Label } from 'ui/label';
import { Observable } from 'data/observable';
import { LabelShadowBaseDirective } from './label-shadow-base.directive';
import { Color } from 'color';

declare const CGSizeMake: any;

@Directive({
selector: '[shadow]'
})
export abstract class LabelShadowDirective extends LabelShadowBaseDirective {

constructor(protected el: ElementRef) {
super(el);
}

protected displayShadowOn(label: Label) {
const nativeView = label.ios;
nativeView.layer.shadowColor = this.shadowColor.ios.CGColor;
nativeView.layer.shadowOffset = CGSizeMake(this.shadowOffset, this.shadowOffset);
nativeView.layer.shadowOpacity = 1.0;
nativeView.layer.shadowRadius = 2.0;
}
}

Затем добавим Directive в декларации AppModule:


// app/app.module.ts

import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
import { NativeScriptModule } from 'nativescript-angular/platform';
import { SocialShareButtonComponent } from './social-share-button/social-share-button.component';
import { TNSFontIconModule } from 'nativescript-ng2-fonticon';
import { AppComponent } from './app.component';
import { LabelShadowDirective } from './label-shadow/label-shadow.directive';

@NgModule({
declarations: [
AppComponent,
SocialShareButtonComponent,
LabelShadowDirective
],
bootstrap: [AppComponent],
imports: [
NativeScriptModule,
TNSFontIconModule.forRoot({
'fa': 'font-awesome.css'
})
],
schemas: [NO_ERRORS_SCHEMA]
})
export class AppModule { }

Теперь мы можем добавить директиву в Label-ы FontIcon, представляющие наши кнопки:


[...]

[...]

[...]

Представим Component в разных размерах и цветах. Отредактируем AppComponent:


// app/app.component.ts

import { Component } from "@angular/core";

@Component({
selector: "my-app",
templateUrl: "app.component.html",
styleUrls: ['app.component.css']
})
export class AppComponent {
public get shareIcons(): Array {
return ['facebook', 'twitter', 'linkedin', 'github', 'tumblr'];
}
}

И шаблон:







Что даст нам:

Создание анимированной кнопки "Поделиться" в NativeScript + Angular

Результат нажатой кнопки «Поделиться»

Кнопки уже выглядят хорошо, но они бесполезны, потому что ничего не делают. Введём EventEmitter Output, который будет показывать имя иконки, когда соответствующая кнопка будет нажата:


// app/social-share-button.component.ts

[...]
@Output('shareButtonTap') shareButtonTap = new EventEmitter();
[...]

Затем привяжем хук к (tap) GridLayout-а кнопки на метод onShareButton(), передавая ему название иконки:


[...]

[...]

Создадим соответствующий метод, показывающий имя значка, передав ему параметром иконку:

// app/social-share-button/social-share-button.component.ts

[...]
public onShareButtonTap(icon: string): void {
this.shareButtonTap.emit(icon);
}
[...]

Это позволяет подписаться на событие в AppComponent:

[...]

[...]

// app/app.component.ts

import { Component } from "@angular/core";
import * as dialogs from 'ui/dialogs';

@Component({
selector: "my-app",
templateUrl: "app.component.html",
styleUrls: ['app.component.css']
})
export class AppComponent {
public get shareIcons(): Array {
return ['facebook', 'twitter', 'linkedin', 'github', 'tumblr'];
}

public onShareButtonTap(event: string): void {
dialogs.alert(`share on: ${event}`);
}
}

Добавим немного проверок

В последнем шаге добавим проверки, для того, чтобы предотвратить некорректное использование Component-а.

Окроем ещё раз SocialShareButton, и сделаем так, чтобы он реализовывал интерфейс OnInit:

// app/social-share-button/social-share-button.component.ts

import {
[...]
OnInit
} from '@angular/core';

[...]
export class SocialShareButtonComponent implements OnInit {
[...]

затем реализуем перехват ngOnInit() с проверками:

// app/social-share-button.component.ts

[...]
public ngOnInit() {
if (!this.shareIcons || this.shareIcons.length === 0) {
throw new Error('you need to specify at least 1 icon');
}
if (this.shareIcons.length > 5) {
throw new Error('the list of icons cannot contain more than 5 elements');
}
}
[...]

Наш Component теперь готов!

Если вам понравился этот материал, не забудьте поделиться им с коллегами!

Источник

20+ Android приложений для прокачки скиллов разработчика, часть 1

20+ Android приложений для прокачки скиллов разработчика, часть 1
Лучший способ учиться — это много читать. И разработчики не исключение. Если вы хотите стать лучше, как разработчик, вы должны читать как можно больше кода. Всё просто.
Книги, блоги, форумы — все они хороши в какой-то степени, но ничто из этого не может заменить реальный рабочий проект с открытым кодом, в котором приложение со всеми ресурсами лежит прямо перед вами.
Всё, что вам нужно, это сесть поудобнее, налить себе кофе и прочитать немного действительно потрясающего кода. Здесь мы упомянем несколько лучших приложений для Android с открытым кодом из разных сфер.

Вы можете для начала установить эти приложения из Play Store, чтобы увидеть, как они работают, перед тем, как окунуться в исходный код. Указанный рядом с каждым приложением уровень сложности должен помочь вам понять, сможете ли вы с ходу погрузиться в него или следует оставить его на потом.

LeafPic

(Github | Play Store | Сложность: Новичок)
20+ Android приложений для прокачки скиллов разработчика, часть 1
Приложения для фото- и видеогалереи есть практически на каждом смартфоне. Вы бы хотели узнать, как они работают? LeafPic это одно из лучших приложений с открытым кодом такого типа.

В его коде очень легко разобраться разработчику любого уровня. А одна из лучших фич этой программы — динамическая смена тем приложения — механизм, который многие разработчики хотели бы сделать в своих приложениях наилучшим образом.

Simple Calendar

(Github | Play Store | Сложность: Новичок)
20+ Android приложений для прокачки скиллов разработчика, часть 1
Простое и лёгкое приложение-календарь, написанное на Kotlin. Если вы планируете начать разрабатывать на Kotlin, то вероятно, это лучший способ.

Лёгкость этого проекта поможет полностью погрузиться в изучение нового языка для разработки приложений Android. Также с ним вы научитесь создавать практически любые виджеты рабочего стола для Android.

Amaze File Manager

(Github | Play Store | Сложность: Продвинутый уровень)
20+ Android приложений для прокачки скиллов разработчика, часть 1
Ещё одно популярное приложение, установленное почти на каждом устройстве, это файловый менеджер.
Поначалу создание такого типа приложения может казаться не сложным, но на деле разработать файлменеджер для всех устройств и версий Android очень нелегко.

Из этого приложения вы узнаете множество полезных вещей, особенно то, как правильно работать с файлами на SD-карте. Однако, я не рекомендую слепо следовать стандартам разработки, применяемым в этой программе — они далеки от идеала.

Easy Sound Recorder

(Github | Play Store | Сложность: Новичок)
20+ Android приложений для прокачки скиллов разработчика, часть 1
Простое, удобное и красивое приложение для звукозаписи под Android. Если вы хотите узнать больше про запись и работу с аудио, то это приложение — отличное начало вашего приключения.

Этот проект очень маленький (в нём всего один Activity) и очень просто для понимания. Новички также узнают из него про основы Material Design.

MLManager

(Github | Play Store | Сложность: Новичок)
20+ Android приложений для прокачки скиллов разработчика, часть 1
MLManager это простой менеджер приложений для Android. Этот проект идеален, если вы хотите научиться получать детальную информацию об установленных приложениях на вашем устройстве, выгрузке файла APK этих приложений, удалении приложений и многом другом.

Стандарты разработки, используемые в этом проекте, очень хороши — им можно следовать неукоснительно и в дальнейшем. Также вы получите представление о разработке простых приложений в стиле гайдов Material Design.

PhotoAffix

(Github | Play Store | Сложность: Новичок)
20+ Android приложений для прокачки скиллов разработчика, часть 1
Очень простое и красиво спроектированное приложение, позволяющее склеить несколько фотографий. Звучит просто? Так и есть.

В идеале любой Android-разработчик должен знать основы Android. Стандарты разработки также должны быть в приоритете. Из этого проекта вы узнаете, как сделать простые, но полезные нестандартные представления, которые помогут вам повысить ваш уровень, чтобы в дальнейшем вы могли создавать по-настоящему сложные и профессиональные представления.

MovieGuide

(Github | Сложность: Продвинутый уровень)

20+ Android приложений для прокачки скиллов разработчика, часть 1
Задача этого приложения предельно проста — выводить список популярных фильмов с трейлерами и обзорами. Но что делает этот проект действительно интересным, так это способ реализации.

В приложении применены такие крутые штуки как

MVP

,

Uncle Bob’s Clean Architecture

,

RxJava

и внедрение зависимостей с

Dagger 2

.

Несмотря на простоту приложения, вы обязательно должны посмотреть на него, хотя бы из-за его фантастической реализации.

AnExplorer

(Github | Play Store | Сложность: Продвинутый уровень)
20+ Android приложений для прокачки скиллов разработчика, часть 1
Еще один простой, лёгкий и минималистичный файловый менеджер, созданный для телефонов и планшетов.

В нём много интересного: работа с файлами, управление в режиме root, загрузчики, нестандартные представления и т.п. Он хорошо спроектирован и вы в принципе должны быстро разобраться в коде.

Minimal ToDo

(Github | Play Store | Сложность: Новичок)

20+ Android приложений для прокачки скиллов разработчика, часть 1

Простенький список дел. Очень простой проект для начала знакомства с разработкой под Android. С ним вы узнаете большинство основных моментов Android-разработки.

Timber

(Github | Play Store | Сложность: Профи)

20+ Android приложений для прокачки скиллов разработчика, часть 1

Timber это прекрасно спроектированый, полнофункциональный аудиоплеер под Android. Если вам когда-нибудь хотелось создать собственное приложение для проигрывания музыки, то этот проект — для вас.

Он находится в активной разработке и очень большой. Для новичка будет довольно сложно с ходу разобраться с тем, что и как в нём происходит, но для продвинутых разработчиков это будет очень интересно.

Источник. Продолжение следует…

Представляем Pipenv!

Я написал новую утилиту на выходных и назвал её pipenv. Зацените её на GitHub!
Представляем Pipenv!
Pipenv это экспериментальный проект, цель которого привнести лучшее из мира packaging в мир Python. Он объединяет такие утилиты как Pipfile, pip и virtualenv в единый инструмент. И очень круто выглядить в терминале.

Pipenv автоматически создаёт и управляет virtualenv в ваших проектах, а также позволяет устанавливать/удалять пакеты Pipfile. А команда lock создаёт lockfile (Pipfile.lock).

Особенности

  • Автоматически находит корень проекта рекурсивным поиском Pipfile.
  • Автоматически создаёт Pipfile, если его ещё нет.
  • Автоматически создаёт файл Pipfile.lock, если его ещё нет.
  • Автоматически создаёт virtualenv в стандартном расположении (project/.venv).
  • Автоматически добавляет пакеты в Pipfile при их установке.
  • Автоматически удаляет пакеты из Pipfile при их удалении.
  • Автоматически обновляет pip.

Главные команды такие: install, uninstall и lock, которая создаёт Pipfile.lock. Они задуманы как замена для $ pip install, а также автоматического управления virtualenv.

Основные концепты

  • virtualenv создаётся автоматически, если её ещё нет.
  • Если не передавать параметров команде install, будут установлены все требуемые пакеты.
  • Если не передавать параметров команде uninstall, все пакеты будут удалены.
  • Для инициализации виртуальной среды Python 3 выполните сперва $ pipenv —three.
  • Для инициализации виртуальной среды Python 2 выполните сперва $ pipenv —two
  • В других случаях основным окружением будет то, которое доступно по команде $ which python.

Другие команды

  • shell создаст оболочку в виртуальной среде.
  • run запустит переданную команду в virtualenv, со всеми переданными аргументами (например, $ pipenv run python).
  • check проверит соответствие зависимостей текущей среды стандарту PEP 508.

Примеры использования

$ pipenv
Usage: pipenv [OPTIONS] COMMAND [ARGS]...

Options:
--where Output project home information.
--bare Minimal output.
--three / --two Use Python 3/2 when creating virtualenv.
--version Show the version and exit.
--help Show this message and exit.

Commands:
check Checks PEP 508 markers provided in Pipfile.
install Installs a provided package and adds it to...
lock Generates Pipfile.lock.
run Spans a command installed into the...
shell Spans a shell within the virtualenv.
uninstall Un-installs a provided package and removes it...
update Updates pip to latest version, uninstalls all...

$ pipenv --where
Pipfile found at /Users/kennethreitz/repos/kr/pip2/test/Pipfile. Considering this to be the project home.

$ pipenv install
Creating a virtualenv for this project...
...
No package provided, installing all dependencies.
Virtualenv location: /Users/kennethreitz/repos/kr/pip2/test/.venv
Installing dependencies from Pipfile.lock...
...

To activate this project's virtualenv, run the following:
$ pipenv shell

$ pipenv install pytest --dev
Installing pytest...
...
Adding pytest to Pipfile's [dev-packages]...

$ pipenv lock
Assuring all dependencies from Pipfile are installed...
Locking [dev-packages] dependencies...
Locking [packages] dependencies...
Note: your project now has only default [packages] installed.
To install [dev-packages], run: $ pipenv init --dev

$ pipenv install --dev
Pipfile found at /Users/kennethreitz/repos/kr/pip2/test/Pipfile. Considering this to be the project home.
Pipfile.lock out of date, updating...
Assuring all dependencies from Pipfile are installed...
Locking [dev-packages] dependencies...
Locking [packages] dependencies...
Note: your project now has only default [packages] installed.
To install [dev-packages], run: $ pipenv install --dev
Installing dependencies from Pipfile.lock...
...

$ pipenv uninstall
No package provided, un-installing all dependencies.
Found 25 installed package(s), purging...
...
Environment now purged and fresh!

$ pipenv shell
Spawning virtualenv shell (/bin/zsh).
(test)$

Установка

$ pip install pipenv
Kenneth Reitz

Генератор скриншотов сайтов на Python

Недавно я столкнулся с довольно простой задачей: создать генератор скриншотов сайтов. Идея такая: вводим любой URL в поле ввода и показываем под ним сгенерированый скриншот страницы.
Для разработки этого приложения я использовал Python/Django и Selenium WebDriver. Selenium это великолепный инструмент для автоматизации веб-задач.
Устанавливаем Python Selenium так:
pip install selenium
Если вы хотите получить скриншот средствами только Python, это так же просто:


from selenium import webdriver

""" Save a screenshot from spotify.com in current directory. """
DRIVER = "chromedriver"
driver = webdriver.Chrome(DRIVER)
driver.get("https://www.spotify.com")
screenshot = driver.save_screenshot("my_screenshot.png")
driver.quit()

Selenium требует драйвера для взаимодействия с браузером и я использую Chrome Driver. Скачайте его отсюда и скопируйте в любую папку вашей PATH, чтобы он был доступен отовсюду.
В моём случае драйвер находится в папке /usr/local/bin/chromedriver
Вы также можете сохранять скриншоты на диск:
driver.save_screenshot(IMAGE_PATH)
или получить скриншот как двоичный объект:
driver.get_screenshot_as_png()
Мы сделаем поддержку обоих вариантов. Начнём разработку приложения:
# Создадим проект
django-admin.py startproject screenshot_generator

# Создадим приложение
django-admin.py startapp app

В файле settings.py не забудьте добавить созданную ‘app’ и задать значения переменных для статических данных.


# settings.py
INSTALLED_APPS = [

‘app’
]

STATIC_URL = ‘/static/’
MEDIA_ROOT = os.path.join(BASE_DIR, ‘media’)
MEDIA_URL = ‘/media/’

В файле urls.py проекта включим url нашего приложения и зададим директорию для медиаконтента:


# urls.py
from django.conf.urls import url, include
from django.conf import settings
from django.views.static import serve
from app import urls as app_urls
urlpatterns = [
url(r’^’, include(app_urls)),
url(r’^media/(?P.*)$’, serve, {
‘document_root’: settings.MEDIA_ROOT,
}),
]

Теперь перейдём к нашему приложению:
Сначала создадим представление главной страницы в файле urls.py:


# urls.py
from django.conf.urls import url
from django.views.generic import TemplateView

urlpatterns = [
url(r’^$’, TemplateView.as_view(template_name=”home.html”)),
]

Затем создадим простой шаблон HTML с формой ввода желаемого URL:

# home.html

Enter URL:

{% csrf_token %}


Как видите, действия в форме вызывают метод get_screenshot, который у нас находится в views.py. В этом методе сконцентрирована вся магия создания скриншота.
Запустим приложение и перейдём по адресу http://localhost:8000/ чтобы увидеть главную страницу:
python manage.py runserver
Генератор скриншотов сайтов на Python
Перед созданием get_screenshot, мы должны добавить представление в файл urls.py. Финальный urls.py будет таким:

# urls.py
from django.conf.urls import url
from django.views.generic import TemplateView
from app import views

urlpatterns = [
url(r’^$’, TemplateView.as_view(template_name=”home.html”)),
url(r’^get_screenshot’, views.get_screenshot, name=”get_screenshot”),
]

Теперь создадим get_screenshot. Пошагово:
# views.py

def get_screenshot(request):
width = 1024
height = 768

Вы можете задать ширину и высоту для драйвера, чтобы получить скриншот нужного размера. Я задаю размеры по-умолчанию для случая, когда они не заданы в URL.
Более подробная документация по API веб-драйвера Selenium и его параметрам находится здесь.
В нашем представлении вначале нам нужно проверить метод запроса и указан ли в нём параметр ‘url’. А также проверить, не пустой ли url и не равен ли null:
if request.method == ‘POST’ and ‘url’ in request.POST:
url = request.POST.get(“url”, “”)
if url is not None and url != ‘’:

Затем проверим параметры url, если пользователь задал их:
params = urlparse.parse_qs(urlparse.urlparse(url).query)
if len(params) > 0:
if ‘w’ in params: width = int(params[‘w’][0])
if ‘h’ in params: height = int(params[‘h’][0])

# Ex: https://www.netflix.com/?w=800&h=600
После этого выберем корректный драйвер, передадим ему url и зададим размер окна:
driver = webdriver.Chrome(DRIVER)
driver.get(url)
driver.set_window_size(width, height)

Теперь проверим, задал ли пользователь параметры сохранения. От этой переменной зависит, будем ли мы сохранять скриншот на диск или хранить его в двоичном формате:
if ‘save’ in params and params[‘save’][0] == ‘true’:

# Ex: https://www.netflix.com/?save=true
Если это условие выполняется, мы сохраним скриншот в папку media. Наименование скриншота будет формироваться склеиванием текущего времени и строки “_image.png”.
Мы должны передать полный путь методу save_screenshot. Также убедимся, что папка для медиаконтента существует:
now = str(datetime.today().timestamp())
img_dir = settings.MEDIA_ROOT
img_name = “”.join([now, ‘_image.png’])
full_img_path = os.path.join(img_dir, img_name)
if not os.path.exists(img_dir):
os.makedirs(img_dir)
driver.save_screenshot(full_img_path)
screenshot = open(full_img_path, “rb”).read()
var_dict = {‘screenshot’:img_name, ‘save’:True}

В случае, если параметр сохранения = false, мы просто получаем двоичные данные методом get_screenshot_as_png() и сохраняем их в специальную переменную:
screenshot = driver.get_screenshot_as_png()
image_64_encode = base64.encodestring(screenshot)
var_dict = {‘screenshot’:image_64_encode}

В обоих случаях var_dict это словарь, содержащий переменные, необходимые для нашего шаблона главной страницы.
В конце уничтожаем объект драйвера и обрабатываем наш шаблон:

# Финальный views.py:
from django.shortcuts import render
from django.http import HttpResponse
from django.conf import settings
from datetime import datetime
from selenium import webdriver

import base64
import os
import urllib.parse as urlparse

DRIVER = “chromedriver”

def get_screenshot(request):
width = 1024
height = 768

if request.method == ‘POST’ and ‘url’ in request.POST:
url = request.POST.get(“url”, “”)
if url is not None and url != ‘’:
params = urlparse.parse_qs(urlparse.urlparse(url).query)
if len(params) > 0:
if ‘w’ in params: width = int(params[‘w’][0])
if ‘h’ in params: height = int(params[‘h’][0])
driver = webdriver.Chrome(DRIVER)
driver.get(url)
driver.set_window_size(width, height)

if ‘save’ in params and params[‘save’][0] == ‘true’:
now = str(datetime.today().timestamp())
img_dir = settings.MEDIA_ROOT
img_name = “”.join([now, ‘_image.png’])
full_img_path = os.path.join(img_dir, img_name)
if not os.path.exists(img_dir):
os.makedirs(img_dir)
driver.save_screenshot(full_img_path)
screenshot = open(full_img_path, “rb”).read()
var_dict = {‘screenshot’:img_name, ‘save’:True}
else:
screenshot = driver.get_screenshot_as_png()
image_64_encode = base64.encodestring(screenshot)
var_dict = {‘screenshot’:image_64_encode}

driver.quit()
return render(request, ‘home.html’, var_dict)
else:
return HttpResponse(“Error”)

Погодите, мы ещё детально не объяснили переменную var_dict и что мы передаём в наш шаблон.
При сохранении скриншота на диск мы передаём название изображения в тег шаблона ‘screenshot’. При получении двоичных данных (в нашем случае там картинка) нам необходимо перекодировать их, чтобы передать в наш словарь в виде строки. Мы не можем передать двоичные данные в метод render.
В Python есть специальный модуль base64, с которым мы легко это сделаем:
import base64

# перекодируем изображение:
screenshot = driver.get_screenshot_as_png()
image_64_encode = base64.encodestring(screenshot)

Для раскодирования создадим файл app_extras.py в папке app/templatetags/ и зарегистрируем шаблонный тег decode_image.
# app_extras.py
from django import template
import base64

register = template.Library

@register.filter()
def decode_image(encoded_image):
return “data:image/png;base64,%s” % encoded_image.decode(“utf8”)

Теперь обновим шаблон home.html для вывода скриншота:

# Финальный home.html
{% load app_extras %}


Enter URL:

{% csrf_token %}


{% if screenshot %}

Screenshot

{% if save %}
Генератор скриншотов сайтов на Python
{% else %}
Генератор скриншотов сайтов на Python
{% endif %}
{% endif %}




Немного пояснений по коду:
Если тег screenshot существует, мы выводим теги HTML. И не будем их выводить если screenshot не задан.
При сохранении скриншота мы показываем его, передавая адрес директории media + наименование изображения как параметр src. Будет что-то подобное:
Генератор скриншотов сайтов на Python
В другом случае мы показываем изображение, прошедшее кодирование и преобразование с помощью шаблонного фильтра, созданного ранее.
Это будет выглядеть примерно так:
Генератор скриншотов сайтов на Python
Запустим наше приложение, чтобы увидеть его в действии:
python manage.py runserver
Для примера я взял адрес сайта Scrapinghub. Я также задал ширину/высоту и сохранение скриншота на диск:
https://www.scrapinghub.com/?w=800&h=600&save=true
Генератор скриншотов сайтов на Python
Исходный код этого проекта вы можете найти на Github.
Надеюсь, этот пример поможет кому-нибудь лучше понять как получать изображения и выводить его на страницу.

Источник

Vue.js — настройка проекта

Vue.js - настройка проекта
Недавно мне удалось поиграться с фреймворком Vue.js. И я был очень впечатлён тем, как лёгок он в настройке и работе. Сильнейшей стороной Vue является лёгкость и простота в настройке. Сейчас я хочу осветить основные моменты по настройке проекта на базе Vue с нуля.

Если вам уже не терпится увидеть готовый проект, можете найти его здесь.

Создаём проект с vue-cli

Самый лёгкий способ создать проект Vue — с помощью vue-cli. Документация о ней здесь. Там есть немало разных шаблонов, с которых можно начать. Я выбрал для себя шаблон на webpack.

Проверьте перед этим, что у вас установлена последняя версия node.js
$ npm install -g vue-cli
$ vue init webpack vuesetup

vue-cli задаст несколько вопросов, которые повлияют на конфигурацию созданного проекта. По завершению установки необходимо скачать все зависимости. Я предпочитаю использовать для этого yarn, но вы можете воспользоваться и npm install.
$ cd vuesetup
$ yarn

ИЛИ

$ cd vuesetup
$ npm install

После этого выполните следующую команду для запуска базового приложения:
$ yarn run dev

ИЛИ

$ npm run dev
Открыв проект в редакторе вы увидите всю проделанную vue-cli работу. Он создал нам базовое приложение с webpack, hot-reload, lint-on-save, unit тестами и другими фичами…всё благодаря команде вокруг vue-cli! Более подробно о том, что сюда включено и как это работает, можно ознакомиться в документации плагина webpack здесь.

Структура проекта

Небольшое замечание по структуре проекта. У каждого есть свои предпочтения по организации проектов. Все мои проекты имеют примерно одинаковую организацию. Я переношу созданные файлы в папку /src:
Vue.js - настройка проекта
Главное, помнить, что контейнеры (containers) будут базовыми страницами приложения [Home / Dashboard / Projects / Login ] и компоненты (components) для этих контейнеров будут храниться в папке components. В крупных проектах я создаю подпапки внутри директории components для более удобной организации связанных компонентов.

Установка Bulma

Bulma это очень удобный и легковесный css фреймворк, к тому же он не имеет никаких прямых JS-зависимостей. Вы можете использовать любой css фреймворк (или вообще работать без него), но я предпочитаю bulma из-за того, что он строит макет (layout) с flexbox (а также в нём отличные компоненты и хорошая документация).

В файле index.html привяжем bulma через cdn:

Vue.js - настройка проекта
Здесь же я привязываю font awesome для иконок

Давайте создадим компонент navbar, используя классы nav от bulma:


Это создаст нам простенькую навигационную панель:
Vue.js - настройка проекта
Также я сделал небольшие правки в файле main.js для привязки приложения. Установим шаблон и импортируем navbar:
import Vue from 'vue'
import navbar from './components/navbar'

/* eslint-disable no-new */
new Vue({
template: `

`,
components: {
navbar
}
}).$mount('#app')

Настройка vue-router

Сейчас у нас есть базовый макет, добавим к нему vue-router и свяжем воедино наши страницы. Вернитесь к окну терминала и введите:
$ yarn add vue-router

ИЛИ

$ npm install vue-router --save
После этого добавьте в корень папки /src файл router.js. В нём мы вызовем наш роутер и опишем роуты нашего приложения:
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

import home from './containers/Home'
import dashboard from './containers/dashboard'
import projects from './containers/projects'

// роуты приложения
const routes = [
{ path: '/', component: home },
{ path: '/dashboard', component: dashboard },
{ path: '/projects', component: projects }
]

// экспорт роутера
export default new Router({
mode: 'history',
routes,
linkActiveClass: 'is-active'
})

Обратите внимание на экспорт роутера — я указал режим history (режим истории). В этом режиме из url убирается хеш. Ещё момент, мы указали linkActiveClass — так мы сообщаем роутеру, какой класс использовать для активации пунктов меню (сильно зависит от применяемого css фреймворка).
Вернёмся к компоненту navbar и заменим теги <а> тегами . Эти теги также преобразуюся в теги <а>, но при этом также указывают роутеру какой css-класс применить в каждом случае.


Далее мы создадим компоненты home, dashboard и project, которые будет загружать наш роутер:




Обновим файл main.js и дадим знать Vue о нашем новом роутере. Таже обратите внимание, я добавил компонент в шаблон — в этом месте роутер будет хранить выбранные роуты.
import Vue from 'vue'
import navbar from './components/navbar'
import router from './router'

/* eslint-disable no-new */
new Vue({
template: `

`,
router,
components: {
navbar
}
}).$mount('#app')

Vue.js - настройка проекта

Настройка vue-resource

После того, как мы сделали роутер, давайте настроим vue-resource, чтобы делать api-вызовы нашего приложения. Вернёмся в консоль и установим vue-resource:
$ yarn add vue-resource

ИЛИ

$ npm install vue-resource --save
Далее откроем файл routes.js, импортируем vue-resource и попросим Vue использовать его:
import Resource from 'vue-resource'

Vue.use(Resource)
После этого вы сможете получать http-ресурсы в ваши компоненты Vue через this.$http. Более подробно об этом можно прочитать в документации по vue-resource здесь. Чтобы продемонстрировать как это работает, сделаем вызов с помощью mock backend (mocky.io) и выведем список проектов на странице project.


Я создал placeholder в секции данных и метод для вызова api. Как только promise вернёт ответ и заполнит объект данными, сразу загрузится наша страничка. Я инициировал вызов один раз только при загрузке компонента.

Настройка axios

Как альтернативу vue-resouce вы можете использовать axios, если хотите. С тех пор как vue-resource перестал быть официальным (подробности здесь), я думаю, стоит предложить альтернативу. Для начала установим axios:
$ yarn add axios

ИЛИ

$npm install axios --save
В отличие от vue-resource, вам не нужно вызывать Vue.use(), так как эта библиотека не привязана напрямую к Vue. Но если вы хотите использовать axios точно так же как vue-resouce, обратите внимание на vue-axios. С ним будет легко добавить axios в объект Vue. А для нашего простого приложения достаточно вызывать axios напрямую. В компоненте Projects.vue я заменил метод загрузки следующим:
methods: {
loadProjects: function () {
axios.get('http://www.mocky.io/v2/585e03ce100000b82c501e8e').then((response) => {
this.projects = response.data
}, (err) => {
console.log(err)
})
}
}

Заключение

Вот так просто мы создали проект с полноценным роутером и возможностью доступа к api нашего бекенда.

Источник