鸿蒙OS&UniApp制作一个小巧的图片浏览器#三方框架 #Uniapp

利用UniApp制作一个小巧的图片浏览器

最近接了个需求,要求做一个轻量级的图片浏览工具,考虑到多端适配的问题,果断选择了UniApp作为开发框架。本文记录了我从0到1的开发过程,希望能给有类似需求的小伙伴一些参考。

前言

移动互联网时代,图片已成为人们日常生活中不可或缺的内容形式。无论是社交媒体还是工作沟通,我们每天都会接触大量图片。因此,一个好用的图片浏览器对用户体验至关重要。

UniApp作为一个使用Vue.js开发所有前端应用的框架,可以实现一套代码、多端运行(iOS、Android、H5、小程序等),是开发跨平台应用的理想选择。今天我就分享一下如何使用UniApp开发一个小巧但功能完善的图片浏览器。

开发环境准备

首先,我们需要搭建UniApp的开发环境:

  1. 安装HBuilderX(官方IDE)
  2. 创建UniApp项目
  3. 配置基础项目结构
bash 复制代码
# 如果使用CLI方式创建项目
npx @vue/cli create -p dcloudio/uni-preset-vue my-image-browser

项目结构设计

为了保持代码的可维护性,我将项目结构设计如下:

复制代码
├── components            # 组件目录
│   ├── image-previewer   # 图片预览组件
│   └── image-grid        # 图片网格组件
├── pages                 # 页面
│   ├── index             # 首页
│   └── detail            # 图片详情页
├── static                # 静态资源
├── utils                 # 工具函数
└── App.vue、main.js等    # 项目入口文件

核心功能实现

1. 图片网格列表

首先实现首页的图片网格列表,这是用户进入应用的第一个界面:

vue 复制代码
<template>
  <view class="container">
    <view class="header">
      <text class="title">图片浏览器</text>
    </view>
    
    <view class="image-grid">
      <view 
        class="image-item" 
        v-for="(item, index) in imageList" 
        :key="index"
        @tap="previewImage(index)"
      >
        <image 
          :src="item.thumb || item.url" 
          mode="aspectFill"
          lazy-load
        ></image>
      </view>
    </view>
    
    <view class="loading" v-if="loading">
      <text>加载中...</text>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      imageList: [],
      page: 1,
      loading: false
    }
  },
  onLoad() {
    this.loadImages()
  },
  // 下拉刷新
  onPullDownRefresh() {
    this.page = 1
    this.imageList = []
    this.loadImages(() => {
      uni.stopPullDownRefresh()
    })
  },
  // 上拉加载更多
  onReachBottom() {
    this.loadImages()
  },
  methods: {
    // 加载图片数据
    loadImages(callback) {
      if (this.loading) return
      
      this.loading = true
      // 这里可以替换为实际的API请求
      setTimeout(() => {
        // 模拟API返回数据
        const newImages = Array(10).fill(0).map((_, i) => ({
          id: this.imageList.length + i + 1,
          url: `https://picsum.photos/id/${this.page * 10 + i}/500/500`,
          thumb: `https://picsum.photos/id/${this.page * 10 + i}/200/200`
        }))
        
        this.imageList = [...this.imageList, ...newImages]
        this.page++
        this.loading = false
        
        callback && callback()
      }, 1000)
    },
    
    // 预览图片
    previewImage(index) {
      const urls = this.imageList.map(item => item.url)
      uni.previewImage({
        urls,
        current: urls[index]
      })
    }
  }
}
</script>

<style>
.container {
  padding: 20rpx;
}

.header {
  height: 80rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 20rpx;
}

.title {
  font-size: 36rpx;
  font-weight: bold;
}

.image-grid {
  display: flex;
  flex-wrap: wrap;
}

.image-item {
  width: 33.33%;
  padding: 5rpx;
  box-sizing: border-box;
}

.image-item image {
  width: 100%;
  height: 220rpx;
  border-radius: 8rpx;
}

.loading {
  text-align: center;
  margin: 30rpx 0;
  color: #999;
}
</style>

这段代码实现了图片瀑布流展示、下拉刷新和上拉加载更多功能。我使用了懒加载技术来优化性能,同时使用缩略图先加载,提升用户体验。

2. 自定义图片预览组件

虽然UniApp内置了图片预览功能,但为了实现更丰富的交互和动画效果,我决定自己封装一个图片预览组件:

vue 复制代码
<template>
  <view
    class="image-previewer"
    v-if="visible"
    @touchstart="handleTouchStart"
    @touchmove="handleTouchMove"
    @touchend="handleTouchEnd"
  >
    <view class="previewer-header">
      <text class="counter">{{ current + 1 }}/{{ images.length }}</text>
      <view class="close" @tap="close">×</view>
    </view>
    
    <swiper
      class="swiper"
      :current="current"
      @change="handleChange"
      :circular="true"
    >
      <swiper-item v-for="(item, index) in images" :key="index">
        <movable-area class="movable-area">
          <movable-view
            class="movable-view"
            :scale="item.scale"
            :scale-min="1"
            :scale-max="4"
            :scale-value="item.scale"
            direction="all"
            @scale="handleScale($event, index)"
            @change="handleMoveChange"
          >
            <image
              :src="item.url"
              mode="widthFix"
              @load="imageLoaded(index)"
            ></image>
          </movable-view>
        </movable-area>
      </swiper-item>
    </swiper>
    
    <view class="previewer-footer">
      <view class="save-btn" @tap="saveImage">保存图片</view>
    </view>
  </view>
</template>

<script>
export default {
  name: 'ImagePreviewer',
  props: {
    urls: {
      type: Array,
      default: () => []
    },
    current: {
      type: Number,
      default: 0
    },
    visible: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      images: [],
      startY: 0,
      moveY: 0,
      moving: false
    }
  },
  watch: {
    urls: {
      handler(val) {
        this.images = val.map(url => ({
          url,
          scale: 1,
          loaded: false
        }))
      },
      immediate: true
    }
  },
  methods: {
    handleChange(e) {
      this.$emit('update:current', e.detail.current)
    },
    
    imageLoaded(index) {
      this.$set(this.images[index], 'loaded', true)
    },
    
    handleScale(e, index) {
      this.$set(this.images[index], 'scale', e.detail.scale)
    },
    
    handleMoveChange() {
      // 处理图片拖动事件
    },
    
    handleTouchStart(e) {
      this.startY = e.touches[0].clientY
    },
    
    handleTouchMove(e) {
      if (this.images[this.current].scale > 1) return
      
      this.moveY = e.touches[0].clientY - this.startY
      if (this.moveY > 0) {
        this.moving = true
      }
    },
    
    handleTouchEnd() {
      if (this.moving && this.moveY > 100) {
        this.close()
      }
      this.moving = false
      this.moveY = 0
    },
    
    close() {
      this.$emit('close')
    },
    
    saveImage() {
      const url = this.images[this.current].url
      // 先下载图片到本地
      uni.downloadFile({
        url,
        success: (res) => {
          // 保存图片到相册
          uni.saveImageToPhotosAlbum({
            filePath: res.tempFilePath,
            success: () => {
              uni.showToast({
                title: '保存成功',
                icon: 'success'
              })
            },
            fail: () => {
              uni.showToast({
                title: '保存失败',
                icon: 'none'
              })
            }
          })
        }
      })
    }
  }
}
</script>

<style>
.image-previewer {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: #000;
  z-index: 999;
  display: flex;
  flex-direction: column;
}

.previewer-header {
  height: 88rpx;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 30rpx;
}

.counter {
  color: #fff;
  font-size: 28rpx;
}

.close {
  color: #fff;
  font-size: 60rpx;
  line-height: 1;
}

.swiper {
  flex: 1;
  width: 100%;
}

.movable-area {
  width: 100%;
  height: 100%;
}

.movable-view {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.movable-view image {
  width: 100%;
}

.previewer-footer {
  height: 100rpx;
  display: flex;
  align-items: center;
  justify-content: center;
}

.save-btn {
  color: #fff;
  font-size: 28rpx;
  padding: 10rpx 30rpx;
  border-radius: 30rpx;
  background-color: rgba(255, 255, 255, 0.2);
}
</style>

这个组件实现了以下功能:

  • 图片切换(使用swiper组件)
  • 图片缩放(movable-view的scale属性)
  • 向下滑动关闭预览
  • 保存图片到本地相册

3. 添加图片相册功能

为了增强应用的实用性,我们来添加一个相册分类功能:

vue 复制代码
<template>
  <view class="album">
    <view class="tabs">
      <view 
        class="tab-item" 
        v-for="(item, index) in albums" 
        :key="index"
        :class="{ active: currentAlbum === index }"
        @tap="switchAlbum(index)"
      >
        <text>{{ item.name }}</text>
      </view>
    </view>
    
    <view class="content">
      <view class="album-info">
        <text class="album-name">{{ albums[currentAlbum].name }}</text>
        <text class="album-count">{{ imageList.length }}张照片</text>
      </view>
      
      <view class="image-grid">
        <view 
          class="image-item" 
          v-for="(item, index) in imageList" 
          :key="index"
          @tap="previewImage(index)"
        >
          <image 
            :src="item.thumb || item.url" 
            mode="aspectFill"
            lazy-load
          ></image>
        </view>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      albums: [
        { id: 1, name: '风景' },
        { id: 2, name: '人物' },
        { id: 3, name: '动物' },
        { id: 4, name: '植物' }
      ],
      currentAlbum: 0,
      imageList: []
    }
  },
  onLoad() {
    this.loadAlbumImages()
  },
  methods: {
    switchAlbum(index) {
      if (this.currentAlbum === index) return
      this.currentAlbum = index
      this.loadAlbumImages()
    },
    
    loadAlbumImages() {
      const albumId = this.albums[this.currentAlbum].id
      
      // 模拟加载不同相册的图片
      uni.showLoading({ title: '加载中' })
      
      setTimeout(() => {
        // 模拟API返回数据
        this.imageList = Array(15).fill(0).map((_, i) => ({
          id: i + 1,
          url: `https://picsum.photos/seed/${albumId * 100 + i}/500/500`,
          thumb: `https://picsum.photos/seed/${albumId * 100 + i}/200/200`
        }))
        
        uni.hideLoading()
      }, 800)
    },
    
    previewImage(index) {
      const urls = this.imageList.map(item => item.url)
      uni.previewImage({
        urls,
        current: urls[index]
      })
    }
  }
}
</script>

<style>
.album {
  display: flex;
  flex-direction: column;
  height: 100vh;
}

.tabs {
  display: flex;
  height: 80rpx;
  border-bottom: 1rpx solid #eee;
}

.tab-item {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
}

.tab-item.active {
  color: #007AFF;
}

.tab-item.active::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: 25%;
  width: 50%;
  height: 4rpx;
  background-color: #007AFF;
}

.content {
  flex: 1;
  padding: 20rpx;
}

.album-info {
  margin-bottom: 20rpx;
}

.album-name {
  font-size: 36rpx;
  font-weight: bold;
}

.album-count {
  font-size: 24rpx;
  color: #999;
  margin-left: 20rpx;
}

.image-grid {
  display: flex;
  flex-wrap: wrap;
}

.image-item {
  width: 33.33%;
  padding: 5rpx;
  box-sizing: border-box;
}

.image-item image {
  width: 100%;
  height: 220rpx;
  border-radius: 8rpx;
}
</style>

性能优化

开发过程中,我注意到一些性能问题,特别是图片加载较慢的情况,因此做了以下优化:

  1. 懒加载:使用lazy-load属性延迟加载图片
  2. 缩略图预加载:先加载小图,再加载大图
  3. 图片压缩:在上传和展示时进行适当压缩
js 复制代码
// 图片压缩工具函数
export function compressImage(src, quality = 80) {
  return new Promise((resolve, reject) => {
    uni.compressImage({
      src,
      quality,
      success: res => {
        resolve(res.tempFilePath)
      },
      fail: err => {
        reject(err)
      }
    })
  })
}
  1. 虚拟列表:当图片数量很多时,考虑使用虚拟列表技术

踩坑记录

开发过程中遇到了一些坑,在此记录,希望能帮助到大家:

  1. 兼容性问题:H5和App表现一致,但在小程序中movable-view的缩放效果不太理想,需要针对不同平台做兼容处理
  2. 图片预览:小程序的图片预览API不支持长按保存,需要自己实现
  3. 内存问题:加载大量高清图片容易导致内存占用过高,需要做好图片资源管理

总结

通过这个项目,我实现了一个简单但功能完善的图片浏览器。UniApp的跨平台能力确实令人印象深刻,一套代码能够同时运行在多个平台上,大大提高了开发效率。

当然,这个应用还有很多可以改进的地方,比如添加图片滤镜、优化动画效果、增加云存储功能等。希望这篇文章对你有所帮助,有任何问题欢迎在评论区留言讨论!

参考资料

  1. UniApp官方文档
  2. Vue.js指南
相关推荐
SuperHeroWu72 小时前
【HarmonyOS 5】鸿蒙碰一碰分享功能开发指南
华为·harmonyos·应用·分享·碰一碰
weixin_545019323 小时前
微信小程序智能商城系统(uniapp+Springboot后端+vue管理端)
spring boot·微信小程序·uni-app
lqj_本人4 小时前
鸿蒙OS&UniApp 实现的语音输入与语音识别功能#三方框架 #Uniapp
uni-app
lqj_本人4 小时前
鸿蒙OS&UniApp 制作动态加载的瀑布流布局#三方框架 #Uniapp
uni-app·harmonyos
a_靖5 小时前
uniapp使用全局组件,
uni-app·全局组件
向明天乄7 小时前
uni-app微信小程序登录流程详解
微信小程序·uni-app
lqj_本人8 小时前
鸿蒙OS&UniApp 开发的下拉刷新与上拉加载列表#三方框架 #Uniapp
华为·uni-app·harmonyos
Lucky me.9 小时前
关于mac配置hdc(鸿蒙)
macos·华为·harmonyos
lqj_本人9 小时前
鸿蒙OS&UniApp 制作个人信息编辑界面与头像上传功能#三方框架 #Uniapp
uni-app·harmonyos