【移动应用开发技术】如何实现一个手帐 APP开发_第1页
【移动应用开发技术】如何实现一个手帐 APP开发_第2页
【移动应用开发技术】如何实现一个手帐 APP开发_第3页
【移动应用开发技术】如何实现一个手帐 APP开发_第4页
【移动应用开发技术】如何实现一个手帐 APP开发_第5页
已阅读5页,还剩10页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

【移动应用开发技术】如何实现一个手帐APP开发

前段时间对手帐类app的实现细节非常感兴趣,遂萌生了想自己实现一个最小化的可行性产品。当然啦~既然是MVP模式下的产品,所以只实现了「功能」,但是在一些自己特别想要去「抄袭」的地方也下了一点功夫去追求UI的表现。前段时间对手帐类app的实现细节非常感兴趣,遂萌生了想自己实现一个最小化的可行性产品。当然啦~既然是MVP模式下的产品,所以只实现了「功能」,但是在一些自己特别想要去「抄袭」的地方也下了一点功夫去追求UI的表现。前言小时候,我是一个手抄报爱好者,四年级的时候班里组织了一个手抄报比赛,老师要求每位同学利用周末的时间做一份手抄报进行评比,主题自选。到现在我印象还非常深刻的是,我想了一个中午都不知道要选什么主题,在白纸上画了一些东西后又全都擦掉了,弄脏了好几张纸,最后画出了一个地球,思路就慢慢打开了。到了周一交给老师的时候,我不敢第一个交,我排在了队伍的最后。老师接到我的手抄报后,居然说:“来来来,你们来看看什么叫手抄报”,我当时的心率达到了极高点,脸又红又烫,站在老师身边站也不是走也不是,尴尬的笑着,但内心却极度自豪。到了初中,班主任也让大家利用周末的时间去做了一个手抄报,因为在小学的时候有了一点经验,再加上到了初中那会儿基本上使用计算机来辅助完成各种任务也都铺开了,我就寻思着能不能再做些创新。当时柯达传出了倒闭的消息,这相当于是一代人的记忆吧~有时候我会跑到老房子里翻到各种胶卷,在阳光的照射下看着映射出的反射图像。结合这个事件,我就想到了利用「胶卷」风格的来阐述对保护鸟类的主题,从网上下载了一些各种鸟类的图片,自己加工一下,终于把手抄报做好了交给老师。当交给老师的那一刻,老师愉悦的笑了,并拿着我的手抄报在讲台上给同学们展示,“大家看下,做的还不错吧~嗯,挺好看!”。高考完的那个暑假,《南国都市报》组织了一次中小学生手抄报大赛,当时我用堂弟的身份参加这个大赛,拿了三等奖,奖品是一张创新书店500元的购书卡。以上就是我对手抄报或者说类似于手帐的这种手工画的经历了,我特别喜欢这种讲述一个故事的方式,可以很好的把我想要表达的东西通过一些文字、图片和画的方式展现出来。所以,当出现了手帐类app时,我迅速的下载进行使用,使用过程中确实达到了自己当初通过组织一些元素和文字来讲述一件事的初衷。前段时间突发奇想,如果我能自己做一个手帐,顺便去探究实现一个手帐app中需要注意的问题,那该多好啊!设计首先,我把AppStore中「手帐」关键词下的搜索排名前10的app都进行了一番使用,总结出了一些手帐app通用点:添加文字。可旋转、放大缩小、旋转字体;添加照片。可旋转翻转、放大缩小、并具备简单或者辅助的图像修饰工具;添加贴纸。使用一些绘制好的贴纸,操作与「添加照片」差不多;模版。提供一套模版,用户可以在这个模版规定好的区域进行内容添加;提供无限长或宽的画布。基本上这些手帐app的功能就是这么多了,因为本着MVP的思路去做这个项目,所以也就没有做到高保真的设计,直接抄了一个比较简洁的手帐app设计。技术栈确定好了自己要实现的大概需要做的功能点后,就需要开始去选择技术栈,因为要做的毕竟是MVP产品而不是demo,我对demo的理解是「实现某个功能点」,对MVP产品的理解是「某个阶段下的完整可用的产品」,MVP模式下出来的东西细节出现一些问题不用太过于苛责,但整体的逻辑上一定是要完整的,不完整的逻辑可以没有,但是一旦有了就要是完整的,覆盖的逻辑路径也可以不是100%,但主逻辑一定要全覆盖。客户端iOSAPP开发技术点如下:纯原生Swift开发;网络请求=>

Alamofire,一些简单的数据直接走

NSFileManager

进行文件持久化管理;UI组件全都基于

UIKit

去做;社会化分享走系统分享,不集成其它SDK;模块上提供「贴纸」、「画笔」、「照片」和「文字」。做的过程中发现其实「照片」和「文字」本质上来说也是贴纸,省了不少事。服务端其实我对自己每新开一个sideproject都有一个硬性要求,做完后要对自己的技术水平有增长,其实「增长」这个东西很玄学,怎么定义「增长」对吧?我给自己找到了一个最简单的思路:用新的东西去完成它!因此在服务端上我就直接无脑的选择了

Vapor

进行,通过Swift去写服务端这是我之前一直想做但找不到时机去做的事情,借此机会就上车了。至于为什么不是选

Perfect,其实我个人没有去动手实践过,只是听大佬们说

Vapor

的API风格比较

Swifty

一些。在第一期的MVP中对服务端的依赖不大,所以目前的架构比较简单,达到能用即可就完事了~关于

Vapor

的一些使用细节,可以在我的这篇文章中进行查看,本文将不再细述

Vapor

使用细节。实现手势对于手帐来说,最核心的一个就是「贴纸」。如何把贴纸从存储中拉出来放到画布上,这一步解决了,后续大部分内容也都解决了。首先,我们需要明确一点,在这个项目中,「画布」本身也是个

UIView,把「贴纸」添加到画布上,实质上就是把

UIImageView

addSubview

UIView

上。其次,手帐中追求的是对素材的控制,可旋转放大是基本操作,而且前文也说过了,我们几乎可以把「照片」和「文字」都认为是对「贴纸」的继承,所以这就抽离出了「贴纸」本身是所以可提供交互组件的基类。手帐类app对贴纸进行多手势操作的流畅性是决定用户留存率很大的一个因素。因此,我们再抽离一下手帐「贴纸」,把基础手势操作都移到更高一层的父类中去,贴纸中留下业务逻辑。手势操作核心代码逻辑如下://

pinchGesture

缩放手势//

缩放的方法(文件私有)。

gesture手势

:UI缩放手势识别器@objcfileprivate

func

pinchImage(gesture:

UIPinchGestureRecognizer)

{

//

当前手势

状态

改变中

if

gesture.state

==

.changed

{

//

当前矩阵2D变换

缩放通过(手势缩放的参数)

transform

=

transform.scaledBy(x:

gesture.scale,

y:

gesture.scale)

//

要复原到1(原尺寸),不要叠加放大

gesture.scale

=

1

}}//

rotateGesture

旋转手势//

旋转的方法(文件私有)。

gesture手势

:UI旋转手势识别器@objcfileprivate

func

rotateImage(gesture:

UIRotationGestureRecognizer)

{

if

gesture.state

==

.changed

{

transform

=

transform.rotated(by:

gesture.rotation)

//

0为弧度制(要跟角度转换)

gesture.rotation

=

0

}}//

panGesture

拖拽/平移手势//

平移的方法(文件私有)。

gesture手势

:UI平移手势识别器@objcfileprivate

func

panImage(gesture:

UIPanGestureRecognizer)

{

if

gesture.state

==

.changed

{

//

坐标转换至父视图坐标

let

gesturePosition

=

gesture.translation(in:

superview)

//

用移动距离与原位置坐标计算。

gesturePosition.x

已经带正负了

center

=

CGPoint(x:

center.x

+

gesturePosition.x,

y:

center.y

+

gesturePosition.y)

//

.zero

CGPoint(x:

0,

y:

0)的简写,

位置坐标回0

gesture.setTranslation(.zero,

in:

superview)

}}//

双击动作(UI点击手势识别器)@objcfileprivate

func

doubleTapGesture(tap:

UITapGestureRecognizer)

{

//

状态

双击结束后

if

tap.state

==

.ended

{

//

翻转

90度

let

ratation

=

CGFloat(Double.pi

/

2.0)

//

变换

旋转角度

=

之前的旋转角度

+

旋转

transform

=

CGAffineTransform(rotationAngle:

previousRotation

+

ratation)

previousRotation

+=

ratation

}}实现的效果下图所示:使用

UICollectionView

作为贴纸容器,通过闭包把点击事件对应索引映射的icon图片实例化为贴纸对象传递给父视图:collectionView.cellSelected

=

{

cellIndex

in

let

stickerImage

=

UIImage(named:

collectionView.iconTitle

+

"\(cellIndex)")

let

sticker

=

UNStickerView()

sticker.width

=

100

sticker.height

=

100

sticker.imgViewModel

=

UNStickerView.ImageStickerViewModel(image:

stickerImage!)

self.sticker?(sticker)}在父视图中通过实现闭包接收贴纸对象,这样就完成了「贴纸」到「画布」的全流程。stickerComponentView.sticker

=

{

$0.viewDelegate

=

self

//

父视图居中

$0.center

=

self.view.center

$0.tag

=

self.stickerTag

self.stickerTag

+=

1

self.view.addSubview($0)

//

添加到贴纸集合中

self.stickerViews.append($0)}「照片」和「文字」手帐编辑页面的底部工具栏之前没做好设计,按道理来说,应该直接上一个

UITabBar

即可完事,但最终也使用了

UICollectionView

完成。读取设备照片操作比较简单,不需要自定义相册,所以通过系统的

UIImagePicker

完成,对自定义相册感兴趣的同学可以看我的这篇文章。顶部工具栏的代码细节如下所示://

底部的点击事件collectionView.cellSelected

=

{

cellIndex

inswitch

cellIndex

{

//

背景

case

0:

self.stickerComponentView.isHidden

=

true

brushView.isHidden

=

true

self.bgImageView.image

=

brushView.drawImage()

self.present(self.colorBottomView,

animated:

true,

completion:

nil)

//

贴纸

case

1:

brushView.isHidden

=

true

self.bgImageView.image

=

brushView.drawImage()

self.stickerComponentView.isHidden

=

false

UIView.animate(withDuration:

0.25,

animations:

{

self.stickerComponentView.bottom

=

self.bottomCollectionView!.y

})

//

文字

case

2:

self.stickerComponentView.isHidden

=

true

brushView.isHidden

=

true

self.bgImageView.image

=

brushView.drawImage()

let

vc

=

UNTextViewController()

self.present(vc,

animated:

true,

completion:

nil)

plateHandler

=

{

viewModel

in

let

stickerLabel

=

UNStickerView(frame:

CGRect(x:

150,

y:

150,

width:

100,

height:

100))

self.view.addSubview(stickerLabel)

stickerLabel.textViewModel

=

viewModel

self.stickerViews.append(stickerLabel)

}

//

照片

case

3:

self.stickerComponentView.isHidden

=

true

brushView.isHidden

=

true

self.bgImageView.image

=

brushView.drawImage()

self.imagePicker.delegate

=

self

self.imagePicker.sourceType

=

.photoLibrary

self.imagePicker.allowsEditing

=

true

self.present(self.imagePicker,

animated:

true,

completion:

nil)

//

画笔

case

4:

self.stickerComponentView.isHidden

=

true

brushView.isHidden

=

false

self.bgImageView.image

=

nil

self.view.bringSubviewToFront(brushView)

default:

break}底部工具栏的每一个模块都是一个

UIView,这部分做的也不太好,最佳的做法应该是基于

UIWindow

或者

UIViewController

做一个「工具容器」作为各个模块UI内容元素的容器,通过这种做法就可以免去在底部工具栏的点击事件回调中写这么多的视图显示/隐藏的状态代码。关注「照片」部分的代码块,实现

UIImagePickerControllerDelegate

协议后的方法为:extension

UNContentViewController:

UIImagePickerControllerDelegate

{

///

从图片选择器中获取选择到的图片

func

imagePickerController(_

picker:

UIImagePickerController,

didFinishPickingMediaWithInfo

info:

[UIImagePickerController.InfoKey

:

Any])

{

//

获取到编辑后的图片

let

image

=

info[UIImagePickerController.InfoKey.editedImage]

as?

UIImage

if

image

!=

nil

{

let

wh

=

image!.size.width

/

image!.size.height

//

初始化贴纸

let

sticker

=

UNStickerView(frame:

CGRect(x:

150,

y:

150,

width:

100,

height:

100

*

wh))

//

添加视图

self.view.addSubview(sticker)

sticker.imgViewModel

=

UNStickerView.ImageStickerViewModel(image:

image!)

//

添加到贴纸集合中

self.stickerViews.append(sticker)

picker.dismiss(animated:

true,

completion:

nil)

}

}}文字文字模块暴露给父视图也是一个实例化后的贴纸对象,不过在文字VC里需要对文字进行颜色、字体和字号的选择。做完了才发现其实因为贴纸是可以通过手势进行放大和缩小的,没必要做字号的选择其中比较费劲的是对文字颜色的选择,刚开始我想的直接上RGB调色就算了,后来想到如果直接通过RGB有三个通道,调起色来非常的难受。想到之前在做《疯狂弹球》这个游戏时使用的HSB颜色模式,做一个圆盘颜色选择器,后来在思考实现细节的过程中了这么EF写的这个库

EFColorPicker,非常好用,改了改UI后直接拿来用了,感谢EF!「气泡视图」的本身是个

UIViewController,但是需要对其几个属性进行设置。其实现流程比较流程化,比较好的做法是封装一下,把这些模版化的代码变成一个「气泡视图」类供业务方使用,但因为时间关系就一直在copy,核心代码如下:///

文字大小气泡private

var

sizeBottomView:

UNBottomSizeViewController

{

get

{

let

sizePopover

=

UNBottomSizeViewController()

sizePopover.size

=

self.textView.font?.pointSize

sizePopover.preferredContentSize

=

CGSize(width:

200,

height:

100)

sizePopover.modalPresentationStyle

=

.popover

let

sizePopoverPVC

=

sizePopover.popoverPresentationController

sizePopoverPVC?.sourceView

=

self.bottomCollectionView

sizePopoverPVC?.sourceRect

=

CGRect(x:

bottomCollectionView!.cellCenterXs[1],

y:

0,

width:

0,

height:

0)

sizePopoverPVC?.permittedArrowDirections

=

.down

sizePopoverPVC?.delegate

=

self

sizePopoverPVC?.backgroundColor

=

.white

sizePopover.sizeChange

=

{

size

in

self.textView.font

=

UIFont(name:

self.textView.font!.familyName,

size:

size)

}

return

sizePopover

}}在需要弹出该气泡视图的地方通过

present

即可调用:collectionView.cellSelected

=

{

cellIndex

in

switch

cellIndex

{

case

0:

self.present(self.fontBottomView,

animated:

true,

completion:

nil)

case

1:

self.present(self.sizeBottomView,

animated:

true,

completion:

nil)

case

2:

self.present(self.colorBottomView,

animated:

true,

completion:

nil)

default:

break

}}画笔之前在滴滴实习时,写过一个关于画笔的组件(居然已经两年前了...),但是这个画笔是基于

drawRect:

方法去做的,对于内存十分不友好,一直画下去,内存就会一直涨,这回采用了

CAShapeLayer

重写了一个,效果还不错。关于画笔的撤回之前基于

drawRect:

的方式去做就会非常简单,每一次的撤回相当于重绘一次,把被撤回的线从绘制点数组中

remove

掉就好了,但基于

CAShapeLayer

实现不太一样,因为其每一笔都是直接生成在

layer

中了,如果需要撤回就得把当前重新生成

layer。所以最后我的做法是每画一笔都去生成一张图片保存到数组中,当执行撤回操作时,就把撤回数组中的最后一个元素替换当前正在的绘制画布内容,并从撤回数组中移除这个元素。有了撤回,那也要把重做给上了。重做的就是防止撤回,做法跟撤回类似。再创建一个重做数组,把每次从撤回数组中移除掉的图片都

append

到重做数组中即可。以下为撤回重做的核心代码://

undo

撤回@objcprivate

func

undo()

{

//

undoDatas

可撤回集合

数量

guard

undoDatas.count

!=

0

else

{

return

}

//

如果是撤回集合中只有

1

个数据,则说明撤回后为空

if

undoDatas.count

==

1

{

//

重做

redo

append

添加

redoDatas.append(undoDatas.last!)

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论