Прошло несколько месяцев с тех пор, как я начал использовать 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 г.