Прошло несколько месяцев с тех пор, как я начал использовать Nativescript Vue, поэтому я подумал, что дам еще несколько советов для новых разработчиков Nativescript. Я обсуждал некоторые из моих первоначальных экспериментов по разработке в последних двух сообщениях в блоге с использованием Firebase и Nativescript Vue для создания базового скелета приложения для социальных сетей. Исходя из этого, я создал Nerdaly, приложение для социальных сетей, написанное на Nativescript Vue с серверной частью NodeJS. Большие изображения приводили к задержкам с загрузкой новых сообщений и медленному рендерингу, поэтому я решил ограничить размер изображений, отправляемых клиентом. В этом посте я расскажу об управлении размерами файлов изображений в Nativescript за счет уменьшения размеров в пикселях для получения меньших (и более быстрых) загрузок файлов изображений.
Сначала я попробовал несколько подходов более высокого уровня, таких как использование другого плагина, а также Параметры ImageAsset при сохранении изображения в файл, но ни один из них не работал стабильно на обеих платформах (и для симуляторов, и для реальных устройства), чтобы ограничить конечную ширину изображения до 400 пикселей. Nativescript абстрагирует объекты изображений в терминах независимых от устройства пикселей с масштабированием для устройств Android и iOS, чтобы предоставить программистам общий интерфейс в Nativescript, но мне требовалось более точное управление конечными размерами. Используя собственный код для каждой платформы, я смог изменить размер файлов изображений до точных размеров перед загрузкой.
Давайте начнем с приложения базового профиля из предыдущего поста, чтобы проиллюстрировать изменения, необходимые для управления сохраненными размерами изображений для изображений из плагинов ImagePicker и Camera. Клонируйте и запустите приложение, используя:
git clone https://github.com/drangelod/nsvfbprofile nsvfbresize
cd nsvfbresize
npm i
tns platform remove ios
tns run ios --bundle
Поскольку с момента публикации исходных сообщений были внесены некоторые важные обновления (в частности, для плагина ImagePicker, чтобы избежать обходных путей и исправить некоторые ошибки iOS), мы сначала обновим приложение, прежде чем переходить к новому коду.
npm install -g nativescript
Это обновит ваш основной интерфейс командной строки Nativescript до версии 4.3 на момент публикации этого сообщения.
tns update
Это обновит ваши основные модули и платформы Android и iOS до версии 5.3.x. Нам также потребуется обновить пакеты NPM и плагины Nativescript, используемые в этом приложении. Обычно я использую NCU tool для сканирования package.json
и сообщаю мне, для каких модулей доступны обновления. ПРИМЕЧАНИЕ. Использование флага -a
укажет NCU обновить все пакеты до последних версий, даже если это серьезное изменение версии, но будьте осторожны, поскольку это может привести к ошибкам из-за критических изменений. Для этого приложения обновление пакета Модули, связанные с webpack, действительно вызовут проблемы и потребуют от вас воссоздания вашего проекта с использованием последней версии шаблона Nativescript Vue. Вместо этого мы обновим только плагины Nativescript и объявления платформы и пока игнорируем обновления, связанные с Vue.
ncu
npm install nativescript-plugin-firebase@latest tns-platform-declarations@latest nativescript-camera@latest nativescript-imagepicker@latest
tns run ios --bundle
Исправление инициализации Firebase
Еще одно важное изменение должно быть сделано для приложений, использующих Firebase с Nativescript Vue, чтобы избежать условий конкуренции между плагином аутентификации Firebase и приложением NSVue watch
в состоянии входа в Firebase. Удалите или закомментируйте блок кода firebase.init()
из /main.js
. Добавьте новое свойство mounted()
с кодом инициализации firebase к вашему объекту export default
в LoginPage.vue
, чтобы он выглядел так:
mounted() {
let that = this;
firebase
.init({
onAuthStateChanged: data => {
console.log(
(data.loggedIn
? "Logged in to firebase"
: "Logged out from firebase") +
" (firebase.init() onAuthStateChanged callback)"
);
if (data.loggedIn) {
that.$backendService.token = data.user.uid;
console.log("uID: " + data.user.uid);
that.$store.commit("setIsLoggedIn", true);
} else {
that.$store.commit("setIsLoggedIn", false);
}
}
})
.then(
function(instance) {
console.log("firebase.init done");
},
function(error) {
console.log("firebase.init error: " + error);
}
);
},
Это гарантирует, что NSVue будет готов увидеть изменение состояния входа из плагина Firebase Auth и правильно перенаправить пользователей, вошедших в систему, на страницу панели мониторинга.
Управление размерами изображения
Ниже представлена исходная chooseImage()
функция для обработки новых изображений профиля, выбранных на устройстве. Поскольку пользователи могут загружать любые изображения, которые находятся на их устройствах, я получил очень широкий диапазон размеров и размеров для сообщений с изображениями. Это вызвало проблемы с хранением, отображением и задержкой в приложении Nerdaly, поэтому я добавил проверку размеров для изменения размера больших изображений. Если у вас нет изображений на вашем текущем симуляторе iOS, загрузите несколько изображений с высоким разрешением с помощью Safari с веб-сайта, такого как NASA, чтобы протестировать их позже.
chooseImage() {
try {
context
.authorize()
.then(() => {
return context.present();
})
.then(selection => {
loader.show();
const imageAsset = selection.length > 0 ? selection[0] : null;
imageAsset.options = {
width: 400,
height: 400,
keepAspectRatio: true
};
imageSourceModule
.fromAsset(imageAsset)
.then(imageSource => {
let saved = false;
let localPath = "";
let filePath = "";
let image = {};
const folderPath = knownFolders.documents().path;
let fileName =
this.$store.state.profile.id +
"-" +
new Date().getTime() +
".jpg";
if (imageAsset.android) {
localPath = imageAsset.android.toString().split("/");
fileName =
fileName +
"_" +
localPath[localPath.length - 1].split(".")[0] +
".jpg";
filePath = path.join(folderPath, fileName);
saved = imageSource.saveToFile(filePath, "jpeg");
if (saved) {
this.pictureSource = imageAsset.android.toString();
} else {
console.log(
"Error! Unable to save pic to local file for saving"
);
}
loader.hide();
} else {
const ios = imageAsset.ios;
if (ios.mediaType === PHAssetMediaType.Image) {
const opt = PHImageRequestOptions.new();
opt.version = PHImageRequestOptionsVersion.Current;
PHImageManager.defaultManager().requestImageDataForAssetOptionsResultHandler(
ios,
opt,
(imageData, dataUTI, orientation, info) => {
image.src = info
.objectForKey("PHImageFileURLKey")
.toString();
localPath = image.src.toString().split("/");
fileName =
fileName +
"_" +
localPath[localPath.length - 1].split(".")[0] +
".jpeg";
filePath = path.join(folderPath, fileName);
saved = imageSource.saveToFile(filePath, "jpeg");
if (saved) {
this.pictureSource = filePath;
} else {
console.log(
"Error! Unable to save pic to local file for saving"
);
}
loader.hide();
}
);
}
}
})
.catch(err => {
console.log(err);
loader.hide();
});
})
.catch(err => {
console.log(err);
loader.hide();
});
} catch (err) {
alert("Please select a valid image.");
console.log(err)
loader.hide();
}
},
Перед загрузкой на сервер нам нужно будет проверить размеры выбранного изображения. Плагин Nativescript ImagePicker возвращает ImageAsset
(представление изображения в памяти в независимых от устройства пикселях). Мы не узнаем фактических размеров изображения, пока ImageAsset
не будет использоваться для создания ImageSource
. Сначала мы добавим проверку ширины изображения после того, как ImageSource будет готов, но перед сохранением и загрузкой. Если ширина превышает 400 пикселей, мы применим собственный код платформы, чтобы изменить размер изображения и сохранить его в файловой системе устройства, которую затем можно будет загрузить в Firebase.
Обновленный раздел кода функции chooseImage () будет выглядеть так:
getSampleSize(uri, options) {
var scale = 1;
if (isAndroid) {
var boundsOptions = new android.graphics.BitmapFactory.Options();
boundsOptions.inJustDecodeBounds = true;
android.graphics.BitmapFactory.decodeFile(uri, boundsOptions);
// Find the correct scale value. It should be the power of 2.
var outWidth = boundsOptions.outWidth;
var outHeight = boundsOptions.outHeight;
if (options) {
var targetSize =
options.maxWidth < options.maxHeight
? options.maxWidth
: options.maxHeight;
while (
!(
this.matchesSize(targetSize, outWidth) ||
this.matchesSize(targetSize, outHeight)
)
) {
outWidth /= 2;
outHeight /= 2;
scale *= 2;
}
}
}
return scale;
},
matchesSize(targetSize, actualSize) {
return targetSize && actualSize / 2 < targetSize;
},
chooseImage() {
let pickcontext = imagepicker.create({ mode: "single" });
try {
pickcontext
.authorize()
.then(() => {
return pickcontext.present();
})
.then(selection => {
const imageAsset = selection.length > 0 ? selection[0] : null;
imageAsset.options = {
width: 400,
keepAspectRatio: true,
autoScaleFactor: false
};
loader.show();
imageSourceModule
.fromAsset(imageAsset)
.then(imageSource => {
var ratio = 400 / imageSource.width;
var newheight = imageSource.height * ratio;
var newwidth = imageSource.width * ratio;
if (imageSource.width > 400) {
console.log(
"Resizing original image dimentions from : " +
imageSource.height +
" x " +
imageSource.width +
" to " +
newheight +
" x " +
newwidth
);
if (isIOS) {
try {
let that = this;
let manager = PHImageManager.defaultManager();
let options = new PHImageRequestOptions();
options.resizeMode =
PHImageRequestOptionsResizeMode.Exact;
options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
manager.requestImageForAssetTargetSizeContentModeOptionsResultHandler(
imageAsset.ios,
{ width: newwidth, height: newheight },
PHImageContentModeAspectFill,
options,
function(result, info) {
let saved = false;
let filePath = "";
const folderPath = knownFolders.documents().path;
let fileName =
that.$store.state.profile.id +
"-" +
new Date().getTime() +
".jpg";
console.log(
"saving image " +
fileName +
" to path " +
folderPath
);
console.log(
"Original image dimentions: " +
imageSource.height +
" x " +
imageSource.width
);
filePath = path.join(folderPath, fileName);
let newasset = new imageAssetModule.ImageAsset(
result
);
imageSourceModule
.fromAsset(newasset)
.then(newimageSource => {
saved = newimageSource.saveToFile(
filePath,
"jpeg"
);
if (saved) {
that.pictureSource = filePath;
that.newFilename = fileName;
console.log(
"Resized image imensions: " +
newimageSource.height +
" x " +
newimageSource.width
);
} else {
console.log(
"Error! Unable to save image to local file for saving"
);
}
loader.hide();
});
}
);
} catch (e) {
console.log("err: " + e);
console.log("stack: " + e.stack);
}
} else if (isAndroid) {
try {
var downsampleOptions = new android.graphics.BitmapFactory.Options();
downsampleOptions.inSampleSize = this.getSampleSize(
imageAsset.android,
{ maxWidth: newwidth, maxHeight: newheight }
);
var bitmap = android.graphics.BitmapFactory.decodeFile(
imageAsset.android,
downsampleOptions
);
imageSource.setNativeSource(bitmap);
let filename =
this.$store.state.profile.id +
"-" +
new Date().getTime() +
".jpg";
let folder = knownFolders.documents();
let fullpath = path.join(folder.path, filename);
let saved = imageSource.saveToFile(fullpath, "jpeg");
if (saved) {
this.pictureSource = fullpath;
this.newFilename = filename;
console.log(
"Resized image imensions: " +
imageSource.height +
" x " +
imageSource.width
);
} else {
console.log(
"Error! Unable to save image to local file for saving"
);
}
loader.hide();
} catch (err) {
console.log(err);
loader.hide();
}
}
} else {
let saved = false;
let filePath = "";
const folderPath = knownFolders.documents().path;
let fileName =
this.$store.state.profile.id +
"-" +
new Date().getTime() +
".jpg";
console.log(
"saving image " + fileName + " to path " + folderPath
);
filePath = path.join(folderPath, fileName);
saved = imageSource.saveToFile(filePath, "jpeg");
if (saved) {
this.pictureSource = filePath;
this.newFilename = fileName;
} else {
console.log(
"Error! Unable to save image to local file for saving"
);
}
loader.hide();
}
})
.catch(err => {
console.log(err);
loader.hide();
});
})
.catch(err => {
console.log(err);
loader.hide();
});
} catch (err) {
alert("Please select a valid image.");
console.log(err);
loader.hide();
}
},
Нам нужно будет изменить импорт в верхней части кода скрипта, чтобы удалить глобальную переменную context
(теперь объявленную локально в функции chooseImage
) и добавить новый импорт для использования модуля ImageAsset
. Если вы запустите версию для iOS и протестируете ее с небольшими и большими изображениями, вы должны увидеть, как приложение изменяет размер больших.
Также были добавлены две новые функции, чтобы помочь с расчетами размеров Android, поскольку это может быть немного более требовательным к масштабируемому разрешению. Теперь вы можете запустить версию Android, чтобы проверить, правильно ли изменяются размеры изображений для этих устройств:
tns platform remove android
tns run android --bundle
Изменение размера изображений камеры
Для Android вы можете применить те же изменения к функции takePicture()
для изменения размера больших фотографий с камеры перед загрузкой в Firebase. Однако для iOS с этим подходом возникает проблема, и он молча умирает, не имея доступа к исходному изображению для изменения размера. Поскольку плагин камеры iOS надежно создает изображения с измененным размером в пределах требований к максимальному размеру, я не слишком глубоко разбирался, почему это не удается, но я предполагаю, что это как-то связано с изолированным доступом к изображениям фотогалереи на iOS для предотвращения прямого манипулирование вызовами платформы. Добавление некоторого дополнительного кода для сохранения изображения в доступный временный файл с последующей его перезагрузкой перед изменением размера, вероятно, сработает, если вам действительно нужен полный контроль для этого сценария.
Новая функция takePicture () будет выглядеть так:
takePicture() {
cameraModule
.takePicture({
width: 400, //these are in device independent pixels
keepAspectRatio: true, // keepAspectRatio is enabled.
saveToGallery: false //Don't save a copy in local gallery, ignored by some Android devices
})
.then(imageAsset => {
imageAsset.options.autoScaleFactor = false;
imageAsset.options.keepAspectRatio = true;
imageAsset.options.width = 400;
//save to file
imageSourceModule.fromAsset(imageAsset).then(
imageSource => {
var ratio = 400 / imageSource.width;
var newheight = imageSource.height * ratio;
var newwidth = imageSource.width * ratio;
if (imageSource.width > 400) {
console.log(
"Resizing original image dimentions from : " +
imageSource.height +
" x " +
imageSource.width +
" to " +
newheight +
" x " +
newwidth
);
if (isIOS) {
console.log("Ignoring resize for camera images on iOS");
let filename =
this.$store.state.profile.id +
"-" +
new Date().getTime() +
".jpg";
let folder = knownFolders.documents();
let fullpath = path.join(folder.path, filename);
let saved = imageSource.saveToFile(fullpath, "jpeg");
if (saved) {
this.pictureSource = fullpath;
this.newFilename = filename;
console.log(
"image imensions: " +
imageSource.height +
" x " +
imageSource.width
);
} else {
console.log(
"Error! Unable to save photo to local file for upload"
);
}
} else if (isAndroid) {
try {
var downsampleOptions = new android.graphics.BitmapFactory.Options();
downsampleOptions.inSampleSize = this.getSampleSize(
imageAsset.android,
{ maxWidth: newwidth, maxHeight: newheight }
);
var bitmap = android.graphics.BitmapFactory.decodeFile(
imageAsset.android,
downsampleOptions
);
imageSource.setNativeSource(bitmap);
let filename =
this.$store.state.profile.id +
"-" +
new Date().getTime() +
".jpg";
let folder = knownFolders.documents();
let fullpath = path.join(folder.path, filename);
let saved = imageSource.saveToFile(fullpath, "jpeg");
if (saved) {
this.pictureSource = fullpath;
this.newFilename = filename;
console.log(
"Resized image imensions: " +
imageSource.height +
" x " +
imageSource.width
);
} else {
console.log(
"Error! Unable to save image to local file for saving"
);
}
loader.hide();
} catch (err) {
console.log(err);
loader.hide();
}
}
} else {
let saved = false;
let filePath = "";
const folderPath = knownFolders.documents().path;
let fileName =
this.$store.state.profile.id +
"-" +
new Date().getTime() +
".jpg";
console.log(
"saving image " + fileName + " to path " + folderPath
);
filePath = path.join(folderPath, fileName);
saved = imageSource.saveToFile(filePath, "jpeg");
if (saved) {
this.pictureSource = filePath;
this.newFilename = fileName;
} else {
console.log(
"Error! Unable to save image to local file for saving"
);
}
loader.hide();
}
},
err => {
console.log("Failed to load from asset");
}
);
})
.catch(err => {
console.error(err);
});
},
Сделанный!
Вот и все, что нужно для этого совета. Если вы хотите скачать окончательные исходные файлы, вы можете найти их на Github.
Первоначально опубликовано на сайте blog.angelengineering.com 3 апреля 2019 г.