В этой статье я расскажу, как создать простое приложение с помощью Nativescript, которое использует камеру устройства для съемки фотографий, отображает эти изображения в простой галерее изображений, использует значки Font Awesome и поддерживает состояние галереи при запуске приложения. Это обновленная версия предыдущего поста, но теперь с использованием Nativescript 6.1.2 с Vue вместо Javascript.

Создание нового приложения с помощью интерфейса командной строки Nativescript

Nativescript 6.1.2 был выпущен недавно, и я буду использовать эту версию в публикации. Я обновился до последней версии интерфейса командной строки, используя следующую команду:

npm install -g nativescript

После установки нового интерфейса командной строки мы создадим новое приложение с помощью интерфейса командной строки Nativescript, выполним диагностическую проверку интерфейса командной строки Nativescript, а затем запустим базовое приложение в симуляторе iOS, чтобы убедиться, что все работает нормально. Мы начнем с настройки нового приложения с именем NSimagallery6, используя следующую команду.

tns create NSimagegallery6

Интерфейс командной строки спросит вас, какой языковой стиль вы хотите использовать и какую структуру шаблонов следует использовать для построения вашего нового приложения. Выберите «Vue.js» в качестве стиля и «Пустой» в качестве шаблона.

После завершения формирования вы можете перейти в этот каталог, добавить платформу iOS, а затем с помощью команды tns doctor CLI убедиться, что среда вашего приложения Nativescript в порядке.

cd NSimagegallery6 
tns platform add ios
tns doctor 

Загруженный шаблон, вероятно, будет нацелен на несколько более старые версии платформы Nativescript, поэтому после запуска tns doctor вы можете увидеть предупреждение, подобное приведенному ниже.

Обновление версии мобильной платформы с помощью интерфейса командной строки Nativescript можно выполнить, удалив платформу и повторно добавив ее в проект с помощью следующих команд. Интерфейс командной строки обновит файл package.json до текущей версии платформы. Кроме того, вы также можете редактировать версии пакетов в package.json вручную.

tns platform remove ios 
tns platform add ios

Запустите приложение на симуляторе или устройстве, используя:

Если все работает нормально, вы должны увидеть следующий экран на симуляторе iPhone.

Затем посмотрите на структуру папок только что созданного приложения, и вы увидите следующее:

Одним из основных отличий этой новой версии шаблона приложения Vue Blank является то, что теперь он предварительно настроен для поддержки использования значков Font Awesome с приложением Nativescript. Вы увидите файл FontAwesome.ttf внутри папки /app/fonts, который загружается /app/app.scss как импорт /app/_app-common.scss и используется через класс CSS с именем fa. Приложение-шаблон использует значок Font Awesome, отображаемый в начале центрального текста. Вы можете найти другие (и более свежие) версии шаблонов приложений в репозитории Nativescript App Templates на Github.

Создание базового приложения для камеры

Приложение-шаблон состоит из одной страницы, на которой отображается страница компонента Vue с простым сообщением в центре, загруженной как основная страница входа в приложение внутри app/app.js. Вы можете включить более подробные отладочные сообщения от интерпретатора Vue в этом файле, добавив Vue.config.silent = false; после исходного кода импорта. Мы хотим, чтобы первая версия нашего приложения была одной страницей, отображающей кнопку для фотографирования, и полем изображения для отображения последней фотографии, сделанной приложением, поэтому мы будем использовать существующую структуру шаблона и заменить содержимое. главной страницы приложения.

Сначала установите Плагин камеры Nativescript с помощью следующей команды CLI, которая позволит нам получить доступ к камере устройства для съемки.

tns plugin add nativescript-camera

Теперь мы изменим компонент домашней страницы in/app/components/Home.vue, чтобы он был:

<template>
    <Page class="page">
        <ActionBar class="action-bar">
            <Label class="action-bar-title" text="Picture Gallery"></Label>
        </ActionBar>
        <StackLayout>
            <Label text="Take a Pic" @tap="takePicture" class="take-picture-icon" />
            <Image :src="cameraImage" class="picture-gallery" stretch="aspectFit" />
        </StackLayout>
    </Page>
</template>
<script>
const cameraModule = require("nativescript-camera");
export default {
    computed: {
    },
    data() {
        return {
            cameraImage: null
        };
    },
    methods: {
        takePicture() {
            let that = this;
            cameraModule
                .takePicture({
                    width: 300,  //these are in device independent pixels
                    height: 300, //only one will be respected depending on os/device if
                    keepAspectRatio: true, //    keepAspectRatio is enabled.
                    saveToGallery: false //Don't save a copy in local gallery, ignored by some Android devices
                })
                .then(imageAsset => {
                    that.cameraImage = imageAsset
                })
        }
    },
    mounted() {
        cameraModule.requestPermissions().then( //request permissions for camera
            success => { //have permissions  
            },
            failure => { //no permissions for camera
            }
        )
    }
}
</script>
<style scoped lang="scss">
.take-picture-icon {
    horizontal-align: center;
    background-color: rgb(105, 105, 241);
    padding: 12;
    border-width: 1.2;
    border-color: black;
    border-radius: 14;
    margin-top: 20;
    color: white;
    font-size: 30;
}
.picture-gallery {
    margin-top: 60;
}
</style>

Мы удалили пример сообщения из шаблона Vue и добавили метку, которая действует и выглядит как кнопка, а также элемент изображения, который будет отображать последнее изображение, сделанное камерой. Внутри раздела кода находится функция takePicture(), которая вызывает плагин Nativescript Camera, чтобы сделать снимок при нажатии кнопки, а затем устанавливает это изображение в качестве источника изображения, отображаемого под кнопкой. Функция mounted() запускается после загрузки страницы и вызывает requestPermissions() в подключаемом модуле камеры, чтобы получить локальные разрешения для приложения для доступа к камере устройства (и библиотеке фотографий при использовании эмулятора iOS для выбора изображения устройства, поскольку в симуляторе нет камеры. ).

Запустите приложение на симуляторе iOS с tns run ios, нажмите кнопку «Сделать фото», выберите изображение из галереи симулятора, и вы должны увидеть что-то похожее на:

Разрешения камеры для сборок Android и iOS

Помимо локальных разрешений, вам нужно будет добавить некоторые дополнительные разрешения для процесса сборки, чтобы разрешить доступ к камере. Для iOS отредактируйте «app / App_Resources / iOS / Info.plist» и добавьте следующие ключи:

<key>NSPhotoLibraryUsageDescription</key>
<string>To access saved pictures</string>
<key>NSCameraUsageDescription</key>
<string>To take new pictures</string>

Для Android отредактируйте «app / App_Resources / Android / AndroidManifest.xml» и добавьте следующее разрешение:

<uses-permission android:name="android.permission.CAMERA"/>

Использование значков Font Awesome с Nativescript

Вместо того, чтобы использовать текст для кнопки с изображением, вы можете использовать значки Font Awesome. Библиотеку Font Awesome можно использовать для внедрения масштабируемых значков в ваше приложение Nativescript, избавляя вас от головной боли при разработке собственных изображений и использования различных версий разрешения, необходимых для всех возможных размеров экрана на Android и iOS. Их также невероятно легко использовать в приложениях Nativescript, тем более что шаблон Blank App поставляется с уже настроенным файлом шрифта и классом css.

Если вы хотите использовать конкретный значок, вам нужно сначала найти значение Unicode для значка. Для этого приложения я буду использовать сплошной значок камеры, поскольку обычная и облегченная версии доступны только как часть пакета Font Awesome Pro. На информационной странице вы увидите юникод для этого значка - f030. Всякий раз, когда вы хотите использовать значок в своем приложении, запишите код, а затем используйте юникод в формате &#xf030; в качестве text.decode значения метки с назначенным классом fa.

Давайте обновим наше приложение этим значком, изменив ярлык с <Label text="Take a Pic" @tap="takePicture" class="take-picture-icon" /> на <Label text.decode="&#xf030; " @tap="takePicture" class="take-picture-icon fa" />.

Сохраните и перезапустите приложение, и теперь оно должно выглядеть так:

Отображение нескольких изображений

Давайте теперь сделаем это приложение более полезным, и вместо того, чтобы отображать только последний сделанный снимок, мы будем использовать простую галерею изображений, чтобы отображать все сделанные на данный момент снимки. Мы воспользуемся директивой Vue v-for для элемента Image и вложим ее в StackLayout для рендеринга всех изображений, сделанных камерой, которые теперь будут храниться в массиве. Он будет вложен в контейнер ScrollView, чтобы мы могли прокручивать изображения вверх и вниз.

Отредактируйте /app/components/Home.vue, чтобы он содержал:

<template>
    <Page class="page">
        <ActionBar class="action-bar">
            <Label class="action-bar-title" text="Picture Gallery"></Label>
        </ActionBar>
        <StackLayout>
            <Label text.decode="&#xf030; " @tap="takePicture" class="take-picture-icon fa" />
            <ScrollView class="picture-gallery" orientation="vertical">
                <StackLayout>
                    <Image v-for="image in arrayPictures" class="gallery-item" :src="image" stretch="aspectFill" />
                </StackLayout>
            </ScrollView>
    
        </StackLayout>
    </Page>
</template>
<script>
const cameraModule = require("nativescript-camera");
export default {
    computed: {},
    data() {
        return {
            arrayPictures: []
        };
    },
    methods: {
        takePicture() {
            let that = this;
            cameraModule
                .takePicture({
                    width: 300, //these are in device independent pixels
                    height: 300, //only one will be respected depending on os/device if
                    keepAspectRatio: true, //    keepAspectRatio is enabled.
                    saveToGallery: false //Don't save a copy in local gallery, ignored by some Android devices
                })
                .then(imageAsset => {
                    that.arrayPictures.unshift(imageAsset)
                })
        }
    },
    mounted() {
        cameraModule.requestPermissions().then( //request permissions for camera
            success => { //have permissions  
            },
            failure => { //no permissions for camera
            }
        )
    }
}
</script>
<style scoped lang="scss">
.take-picture-icon {
    horizontal-align: center;
    background-color: rgb(105, 105, 241);
    padding: 12;
    border-width: 1.2;
    border-color: black;
    border-radius: 14;
    margin-top: 20;
    margin-bottom: 20;
    color: white;
    font-size: 30;
    padding-left: 20;
}
.picture-gallery {
    margin-top: 20;
}
.gallery-item {
    margin: 10;
}
</style

Функция takePicture() теперь будет сохранять каждое изображение, сделанное в массиве arrayPictures (unshift используется вместо push для отображения первой фотографии в первую очередь). Nativescript Vue теперь будет отображать изображение для каждого элемента массива внутри прокручиваемой области под кнопкой.

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

Добавить страницу сведений для аннотации и удаления

Пока что у нас есть простое приложение для камеры с галереей изображений, но оно довольно простое. Давайте сделаем его более полезным, добавив новую страницу сведений для просмотра изображения в полноэкранном режиме при нажатии на элемент галереи. Затем на этой новой странице мы добавим поддержку для аннотирования каждого изображения (с примечанием, отображаемым поверх каждого изображения галереи) и кнопку для удаления изображения из галереи.

Во-первых, давайте добавим текстовое поле примечания, которое будет отображаться в ярлыке над каждым изображением в нашей галерее, заменив тег изображения (<Image v-for="image in arrayPictures" class="gallery-item" :src="image" stretch="aspectFill"/>) в /app/components/Home.vue на:

<GridLayout cols="*" rows="*" @tap="tapPicture" v-for="image in arrayPictures">
    <Image class="gallery-item" col="0" row="0" :src="image" stretch="aspectFill" />
    <StackLayout :visibility="image.note&&(image.note.length>0)  ? 'visible' : 'hidden'" col="0" row="0" class="note-picture-wrapper">
        <Label textWrap="true" verticalAlignement="bottom" :text="image.note" class="note-picture-text"></Label>
    </StackLayout>
</GridLayout>

GridLayout используется для наложения метки на изображение, размещая их в одной строке и столбце. Label вложен внутрь StackLayout, чтобы можно было стилизовать его, чтобы текстовая область могла быть затененной и полупрозрачной. Заявление v-if в метке гарантирует, что этот закрашенный оверлей будет отображаться только в том случае, если для этого изображения есть какой-либо текст. Нам нужно будет добавить несколько новых классов стилей в конец /app/components/Home.vue для новой метки:

.note-picture-wrapper {
  background-color: #1a1919;
  opacity: 0.7;
  border-width: 1;
  border-radius: 8;
  color: #ffffff;
  border-color: #ffffff;
  margin: 15;
  vertical-align: bottom;
  horizontal-align: center;
}
.note-picture-text {
  font-size: 15;
  vertical-align: center;
  horizontal-align: center;
  padding: 4;
}

Теперь мы добавим новую функцию tapPicture(), а также обновим функцию takePicture() для присвоения каждому изображению уникального идентификатора (установленного на текущую временную метку Unix и используемого в качестве ключа в директиве v-for), а также инициализируем связанную пустую строку заметки. с каждым изображением. Измените раздел methods:{} на:

    methods: {
        tapPicture(image) {
            let navContextObj = {
                image: image,
                arrayPictures: this.arrayPictures
            };
            this.$navigateTo(ImageDetails, {
                animated: true,
                transition: {
                    name: "slideLeft",
                    curve: "easeInOut",
                    duration: 100
                },
                props: { navObject: navContextObj }
            });
        },
        takePicture() {
            let that = this;
            cameraModule
                .takePicture({
                    width: 300, //these are in device independent pixels
                    height: 300, //only one will be respected depending on os/device if
                    keepAspectRatio: true, //    keepAspectRatio is enabled.
                    saveToGallery: false //Don't save a copy in local gallery, ignored by some Android devices
                })
                .then(imageAsset => {
                    imageAsset.note = ''
                    imageAsset.id = new Date().getTime()
                    that.arrayPictures.unshift(imageAsset)
                })
        }
    },

Функция tapPicture перейдет на новую страницу (/app/components/ImageDetails.vue) и передаст нажатое изображение и массив галереи изображений в качестве опоры. Передача их в качестве членов объекта гарантирует, что ссылка на исходный массив изображений будет передана для использования на странице сведений. Давайте создадим этот файл и отредактируем его, чтобы он содержал:

<template>
    <Page class="page" ref="page" actionBarHidden="false" backgroundSpanUnderStatusBar="true">
    
        <ActionBar class="action-bar" title="Picture Details">
            <NavigationButton text="Done" android.systemIcon="ic_menu_back" @tap="$navigateBack()" />
            <Label class="action-bar-title" text="Picture Details"></Label>
        </ActionBar>
        <ScrollView orientation="vertical">
            <StackLayout>
                <Image class="picture-full" stretch="aspectFit" :src="navObject.image" />
                <GridLayout columns="*,*" rows="60,30,*,300">
                    <StackLayout col="1" row="0" class="delete-picture-icon-wrapper" @tap="deletePicture">
                        <Label verticalAlignement="bottom" text="delete" class="delete-picture-icon"></Label>
                    </StackLayout>
                    <Label col="0" colSpan="2" row="1" text="Note:" class="section-label" />
                    <TextView col="0" colSpan="2" row="2" class="text-picture" hint="Add a note for this picture here" editable="true" v-model="navObject.image.note" />
                    <Label col="0" colSpan="2" row="3" text="" />
                </GridLayout>
            </StackLayout>
        </ScrollView>
    </Page>
</template>
<script>
export default {
    name: "image-details-page",
    data() {
        return {};
    },
    props: {
        navObject: {
            type: Object
        },
    },
    components: {},
    computed: {},
    created() {},
    mounted() {},
    methods: {
        deletePicture() {
            let pictureIndex = this.navObject.arrayPictures.indexOf(this.navObject.image);
            this.navObject.arrayPictures.splice(pictureIndex, 1);
            this.$navigateBack()
        }
    }
};
</script>
<style scoped>
.delete-picture-icon {
    font-size: 15;
    vertical-align: center;
    horizontal-align: center;
}
.delete-picture-icon-wrapper {
    background-color: #000000;
    border-width: 1;
    border-radius: 8;
    color: #ffffff;
    border-color: #ffffff;
    margin: 15;
    vertical-align: center;
    horizontal-align: right;
    height: 30;
    width: 60;
}
.text-picture {
    border-width: 1;
    border-style: solid;
    border-color: #01060c;
    height: 80;
    background-color: rgb(235, 233, 233);
}
.section-label {
    background-color: #292b2b;
    border-width: 1;
    border-style: solid;
    border-color: #01060c;
    color: white;
    padding-left: 10;
    padding-top: 5;
    padding-bottom: 5;
}
.picture-full {
    border-width: 1;
    border-color: gray;
}
</style>

Глядя на XML в верхнем разделе, вы увидите, что элемент ActionBar похож на главную страницу, но теперь мы добавляем NavigationButton, чтобы пользователь мог вернуться на первую страницу. Ниже мы отображаем изображение, нажатое на первой странице, кнопку удаления и текстовую область, чтобы пользователь мог ввести примечание (привязанное к переменной примечания объекта изображения) для выбранного изображения. Дополнительная метка внизу с присвоенной GridLayout высотой 300 предназначена для обеспечения некоторого буферного пространства, когда экранная клавиатура всплывает во время внесения изменений в текст заметки с изображением. Функция deletePicture() - единственный метод на этой странице, который удаляет текущее изображение из массива галереи изображений перед возвращением на главную страницу.

Поддерживать состояние галереи изображений

Теперь у нас есть функциональное приложение для создания галереи изображений, которое позволяет нам добавлять новые изображения с камеры, комментировать их и удалять изображения. Самая большая проблема сейчас в том, что когда мы закроем приложение и снова откроем его, все изображения и их заметки исчезнут. Наш последний шаг к созданию полезного приложения - поддержание состояния изображений и связанных с ними заметок при запуске приложения. Модуль Nativescript ApplicationSettings позволяет нам сохранять и загружать текстовые данные из локального хранилища приложения, поэтому мы можем использовать их для поддержания состояния при запуске приложения. Чтобы использовать его для этой галереи изображений, нам нужно будет начать сохранять изображения в виде файлов на устройстве, поскольку с помощью этого модуля мы не можем легко хранить двоичные данные. Вместо этого мы будем использовать этот модуль для хранения массива имен файлов и заметок с отслеживанием состояния, который обновляется при каждом изменении и перезагружается при следующем запуске приложения.

Мы будем использовать еще несколько библиотечных модулей, поэтому добавьте следующее в начало раздела кода in/app/components/Home.vue:

const applicationSettings = require("application-settings"); 
const fsModule = require("tns-core-modules/file-system"); 
const imageSourceModule = require("tns-core-modules/image-source");

applicationSettings будет использоваться для сохранения и загрузки состояния приложения на устройстве. fsModule будет использоваться для сохранения и загрузки изображений в хранилище локального устройства при использовании с imageSourceModule, который используется для работы с изображениями.

Измените метод takePicture(), чтобы он теперь содержал следующее:

    takePicture() {
            let that = this;
            cameraModule
                .takePicture({
                    width: 300, //these are in device independent pixels
                    height: 300, //only one will be respected depending on os/device if
                    keepAspectRatio: true, //    keepAspectRatio is enabled.
                    saveToGallery: false //Don't save a copy in local gallery, ignored by some Android devices
                })
                .then(imageAsset => {
                    imageSourceModule.fromAsset(imageAsset).then(
                        savedImage => {
                            let filename = "image" + "-" + new Date().getTime() + ".png";
                            let folder = fsModule.knownFolders.documents();
                            let path = fsModule.path.join(folder.path, filename);
                            savedImage.saveToFile(path, "png");
                            var loadedImage = imageSourceModule.fromFile(path);
                            loadedImage.filename = filename;
                            loadedImage.note = "";
                            that.arrayPictures.unshift(loadedImage);
                            that.storeData();
                        },
                        err => {
                            console.log("Failed to load from asset");
                        }
                    );
                })
        },

Нам также потребуются новые методы для загрузки и сохранения текущего состояния галереи изображений. Добавьте следующие функции в раздел methods:{}

    storeData() {
            let localArr = [];
            this.arrayPictures.forEach(entry => {
                localArr.push({ note: entry.note, filename: entry.filename });
            })
            applicationSettings.setString("localdata", JSON.stringify(localArr));
        },
        loadData() {
            let strData = applicationSettings.getString("localdata");
            console.log(strData)
            if (strData && strData.length) {
                let localArr = JSON.parse(strData);
                localArr.forEach(entry => {
                    const folder = fsModule.knownFolders.documents();
                    const path = fsModule.path.join(folder.path, entry.filename);
                    var loadedImage = imageSourceModule.fromFile(path);
                    loadedImage.filename = entry.filename;
                    loadedImage.note = entry.note;
                    this.arrayPictures.unshift(loadedImage);
                })
            }
        },

После того, как мы сделали снимок, мы сохраним его как файл изображения в хранилище локального устройства приложения и будем использовать Image Asset, загруженный из сохраненного изображения. Каждый раз, когда вносятся изменения путем добавления или удаления изображения, мы вызываем новую функцию storeData, чтобы сохранить массив заметок к изображениям и имен файлов в локальном хранилище.

Когда изображения удаляются из галереи с помощью deletePicture или когда их примечания были изменены, нам необходимо обновить сохраненное состояние. Мы можем справиться с обоими из них, обновив состояние непосредственно перед возвращением со страницы сведений. Мы будем использовать ловушку жизненного цикла beforeDestroy Vue на странице сведений для вызова нашей storeData функции. Поскольку эта функция определена на главной странице, нам также необходимо передать ссылку на эту функцию как часть контекста навигации. Добавьте следующее в export default { в /app/components/ImageDetails.vue

beforeDestroy() { 
    this.navObject.storeData() 
},

Мы передадим эту ссылку на функцию как часть навигационного контекста главной страницы, изменив tapPicture в /app/components/Home.vue:

let navContextObj = { 
    image: image, arrayPictures: this.arrayPictures, storeData:this.storeData 
};

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

Выполнено!

Это все для этого поста. Если вы хотите скачать окончательные исходные файлы, вы можете найти их на Github.

Первоначально опубликовано на https://blog.angelengineering.com 1 ноября 2019 г.