本周是跟着尚硅谷的教程进行了一个实践项目的编写,相比于黑马的项目,我个人觉得尚硅谷的项目会比较的简单,结构比较清晰。更多使用@Builder和@Component,将组件封装在page页面中,好处是更容易寻找,而且不会有那么多的文件,坏处是代码会显得很冗长,而且代码的复用性会比较差。
本周的项目是一个单词打卡项目,包括了登录、打卡、答题等页面,是一个相对完整的项目,但这个项目更像一个纯前端项目,通过调用后端接口,而不是建表和操作数据库来进行数据的交互与存储。
欢迎页
这个页面非常的简单,就是一个背景图片加上一个转场动画。
背景设置
- .backgroundImage($r('app.media.img_splash_bg'))
- .backgroundImageSize({ width: '100%', height: '100%' })
转场动画
转场动画的详细介绍见第三周。
- if(this.flag){
- Image($r('app.media.ic_logo'))
- .logoStyle()
- .transition({type:TransitionType.Insert,opacity:0,translate:{x:-150}})
- Text('快速单词记忆神器')
- .titleStyle()
- .transition({type:TransitionType.Insert,opacity:0,translate:{x:150}})
- }
关键在于调用transition,并设置各种自定义属性。如type可以设置进场动画或者是出场动画。
跳转和动画启动逻辑
- onPageShow(){
- animateTo({duration:1000,onFinish:()=>{
- setTimeout(()=>{
- router.replaceUrl({url:'pages/Index'})
- },200)
- }},()=>{
- this.flag=true;
- })
- }
当我们用transition设置好动画样式后,我们要用animateTo设置动画的运行时间 。
onfinish回调函数,用于设置动画完成后的逻辑。动画完成2s后,我们要跳转到到主页,所以用路由router的replaceUrl进行跳转。(用replaceUrl的原因是:欢迎页面一般只有在进入APP时要调用,后续不会再出现)
flag是控制动画开始与否的变量,当flag为true时,才会显示上面的Image和text,动画才会显示。
最外层的onPageShow是生命周期函数,当页面展示时会调用这个函数。
答题页
(细心的友友可能发现这个页面的停止测试的止不见了,还有一个奇怪的答案灌,这个我也不太清楚是为什么,在预览器的时候是正常的,但是用模拟器跑的时候就会这样,其他项目也没有问题。知道是为什么的友友麻烦评论区跟我说一下,谢谢大家!)
在这个页面中,我们可以看到底部又有熟悉的tabbar页面切换,这应该是每个手机软件的标配了。用于切换不同的主要页面。除此之外主要有三个部分,统计部分,单词部分,选项部分。
统计部分
由图可见,这四个部分的结构都是相同的,由一个icon加上text,中间一段空白,最后再有一个不同的组件。所以我们应该将这个结构单独封装起来,简化代码,提高复用性。但是我们最后有一个不同组件,要怎么封装哇,这时候我们就要引入一个新的装饰器@BuilderParam
@BuilderParam装饰的方法只能被自定义构建函数(@Builder装饰的方法)初始化。什么意思呢,就是我们可以用@Builder封装起来的内容,就可以传入到@BuilderParam装饰的方法中。
所以代码如下图所示:
- @Component
- export struct StatItem {
- icon:Resource
- name:string
- @BuilderParam statComp:()=>void
- fontColor:Color
- build() {
- Row({space:10}){
- Image(this.icon)
- .height(14)
- .width(14)
- Text(this.name)
- .fontWeight(FontWeight.Medium)
- .fontSize(14)
- .fontColor(this.fontColor)
- Blank()
- this.statComp()
- }
- .width('100%')
- .height(30)
- }
- }
以“进度”举例:
- StatItem({
- icon:$r('app.media.ic_progress'),
- name:'进度',
- fontColor:Color.Black
- }){
- Progress({value:this.answeredCount,total:this.totalCount})
- .width(100)
- }
在传入其他参数后,我们再传入一个对象,这个对象就和我们编写@builder一样,传入自定组件机器样式即可,如这个代码中就传入了一个进度条组件Progress。
同时我们每次每次完成一次答题,就会有一个记录我们答题情况的弹窗,这个弹窗的结构也和统计页面的样式相似,也可以复用这个结构。
个数
个数中的自定义组件是一个button组件,点击后会出现一个弹窗,是一个TextPickerDialog,是一个文本滑动弹窗。
用于设置本次答题组中包含多少个单词。
- StatItem({
- icon:$r('app.media.ic_count'),
- name:'个数',
- fontColor:Color.Black
- }){
- Button(this.totalCount.toString())
- .width(100)
- .height(25)
- .backgroundColor('#EBEBEB')
- .enabled(this.practiceStatus===PracticeStatus.Stopped)
- .fontColor(Color.Black)
- .onClick(()=>{
- TextPickerDialog.show({
- range:['5','10','15','20'],
- value:this.totalCount.toString(),
- onAccept:(result)=>{
- this.totalCount=parseInt(result.value)
- this.questions=getRandomQuestions(this.totalCount)
- }
- })
- })
这里面有个enabled属性,标识什么时候这个组件可以被点击。practiceStatus是一个用于标识单词测试状态的变量,有停止、暂停、进行三个状态,只有状态为停止才可以进行修改。因为我们不可以测试测一半进行修改,这不符合逻辑。TextPickerDialog中的onAccept就是我们点击确定后的逻辑。getRandomQuestions是自定义的一个函数,用于从题库中抽取用户设置的题目数量的题。
- export function getRandomQuestions(count: number) {
- let length = questionData.length;
- let indexes: number[] = [];
- while (indexes.length < count) {
- let index = Math.floor(Math.random() * length);
- if (!indexes.includes(index)) {
- indexes.push(index)
- }
- }
- return indexes.map(index => questionData[index])
- }
计时器
计时器中用到了一个新的组件TextTimer,是一个通过文本显示计时信息并控制其计时器状态的组件。
- TextTimer({ controller: this.timerController })
- .onTimer((utc,elapsedTime)=>{
- this.timeUsed=elapsedTime
- })
需要设置一个控制组件controller,需要提前进行声明:
timerController: TextTimerController = new TextTimerController();
声明后timerController将会有start、pause、stop三个属性,对应我们所定义的开始、暂停、停止。
onTimer事件时间文本发生变化时触发,elapsedTime:计时器经过的时间,单位为毫秒。
单词部分
这个部分就很简单了,就是两个text的组件,代码如下:
- Column(){
- Text(this.questions[this.currentIndex].word)
- .wordStyle()
- Text(this.questions[this.currentIndex].sentence)
- .sentenceStyle()
- }
选项部分
因为四个选项的样式逻辑全都相同,且储存在数组中,所以我们可以使用foreach循环来进行渲染
- ForEach(this.questions[this.currentIndex].options,(option)=>{
- OptionButton({
- option: option,
- answerStatus:this.answerStatus,
- answer:this.questions[this.currentIndex].answer,
- selectedOption:this.selectedOption
- })
- .enabled(this.answerStatus==AnswerStatus.Answering)
- .onClick(()=>{
- if(this.practiceStatus!==PracticeStatus.Running){
- promptAction.showToast({message:'请先开始测试'})
- return
- }
-
- this.selectedOption=option
-
- this.answeredCount++
- if(option===this.questions[this.currentIndex].answer){
- this.rightCount++
- }
-
- this.answerStatus=AnswerStatus.Answered
- if(this.currentIndex<this.questions.length-1){
- setTimeout(()=>{
- this.currentIndex++
- this.answerStatus=AnswerStatus.Answering
- },500)
- }else{
- this.stopPractice()
- }
- })
- },option => this.questions[this.currentIndex].word + '-' + option)
OptionButton是封装的组件,也就是每个选项的样式和逻辑,如下:
- @Component
- struct OptionButton{
- option:string
- answer:string
- @State optionStatus:OptionStatus=OptionStatus.Default
- //注意!!声明顺序影响更新顺序,会导致出错
- @Prop selectedOption:string
- @Prop @Watch('onAnswerStatus') answerStatus:AnswerStatus
-
- onAnswerStatus(){
- if (this.option===this.answer) {
- this.optionStatus=OptionStatus.Right
- }else{
- if(this.option===this.selectedOption){
- this.optionStatus=OptionStatus.Wrong
- }else{
- this.optionStatus=OptionStatus.Default
- }
- }
- }
-
- getBgColor(){
- switch (this.optionStatus) {
- case OptionStatus.Right:
- return '#1DBF7B'
- case OptionStatus.Wrong:
- return '#FA635F'
- default:
- return Color.White
- }
- }
- build(){
- Stack(){
- Button(this.option)
- .optionButtonStyle({
- bg: this.getBgColor(),
- font: this.optionStatus === OptionStatus.Default ? Color.Black : Color.White
- })
- if(this.optionStatus===OptionStatus.Right){
- Image($r('app.media.ic_right'))
- .width(22)
- .height(22)
- .offset({x:10})
- }else if (this.optionStatus===OptionStatus.Wrong){
- Image($r('app.media.ic_wrong'))
- .width(22)
- .height(22)
- .offset({x:10})
- }
- }.alignContent(Alignment.Start)
- }
- }
首先我们要明确点击选项后我们需要触发什么逻辑操作:1、我们要判断所选项是否为正确选项;2、如果为正确选项,则该选项要变绿,不为正确选项则要变红,且正确选项要变绿;3、在选择后要跳转到下一题,若已经是最后一题则要跳出弹窗。
onAnswerStatus用于判断按键状态,判断所选是否为正确,getBgColor用于给按键添加背景颜色,正确或错误的背景色。answerStatus是用于监测用户是否已经点击了选项,监听到状态变化则触发onAnswerStatus。
- if(this.currentIndex<this.questions.length-1){
- setTimeout(()=>{
- this.currentIndex++
- this.answerStatus=AnswerStatus.Answering
- },500)
- }else{
- this.stopPractice()
- }
这段代码则是用于判断是否为本测试组的最后一题,如果是的话,则调用停止逻辑:
- stopPractice(){
- this.practiceStatus = PracticeStatus.Stopped
- //停止计时器
- this.timerController.pause()
- //弹窗
- this.dialogController.open()
- }
弹窗内容就是前面讲到的完成答题后的弹窗。
Tabs页面切换
- Tabs({ index: this.currentTabIndex }){
- TabContent(){
- PracticePage()
- }.tabBar(this.barBuilder(0,'答题',$r('app.media.ic_practice'),$r('app.media.ic_practice_selected')))
-
- TabContent(){
- CirclePage()
- }.tabBar(this.barBuilder(1,'圈子',$r('app.media.ic_circle'),$r('app.media.ic_circle_selected')))
-
- TabContent(){
- MinePage()
- }.tabBar(this.barBuilder(2,'我的',$r('app.media.ic_mine'),$r('app.media.ic_mine_selected')))
- }
用Tabs搭配TabContent和tabbar使用,TabContent中用于存放页面的UI界面 ,tabbar则是定义页面切换导航的样式,详见第五六周的文章。本代码中,把每个页面详细的样式封装成了单独的组件,然后用import引入后使用。
tabbar样式
- @Builder barBuilder(index:number,title:string,icon:Resource,iconSelected:Resource){
- Column(){
- Image(this.currentTabIndex === index ? iconSelected : icon)
- .width(25)
- .height(25)
- Text(title)
- .tabTitleStyle(this.currentTabIndex === index ? Color.Black:'#959595')
- }
- }
这里自定义了tabbar 的样式,让其为图片加文字的组合。并且当tabbar被选中时会有不同其他tabbar的效果。
评论记录:
回复评论: