前言
DevEco Studio版本:4.0.0.600
WanAndroid的API链接:玩Android 开放API-玩Android - wanandroid.com
为了兼容HarmonyOS,我这边以Arkts--API9为例进行实现
通过华为官网渠道目前下载的版本还是3.1的,这边提供下4.0版本下载的渠道:
DevEco Studio 4.0版本下载地址:DevEcoStudio 4.0版本下载
4.0版本的配套关系
注:一些基础知识点和默认配置项不做详细说明了,本文基于大家有一定的鸿蒙基础能力。有不明白的可以留言咨询
实现效果:
玩安卓演示视频
项目创建
为了兼容HarmonyOS(目前手机API版本只到9),compile SDK : 9
模块构建
根据项目功能模块划分了:HomeModule、ProjectModule、NavigationModule、MineModule四个功能模块,加上基础模块BaseLibrary和主模块entry组成项目的基础架构。
File-->new-->Module新建模块
其他几个模块依照上图为例依次创建,创建后如下图所示:
BaseLibrary资源链接:https://download.csdn.net/download/Abner_Crazy/88908326
项目页面实现
1、启动页面(SplashPage)
新建SplashPage页面:new-->Page创建页面,或者new-->ArkTS File创建文件(需要手动在main_pages.json中添加页面引用)
SplashPage详细代码:
import router from '@ohos.router'
import LogUtils from '@app/BaseLibrary/src/main/ets/utils/LogUtils'
@Entry
@Component
struct SplashPage {
@State counter: number = 5
@State message: string = 'Wan Android'
private intervalID: number = -1
build() {
RelativeContainer() {
Text(this.message)
.fontSize(52)
.fontWeight(FontWeight.Bold)
.id('textContent')
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
//倒计时
Text(`${this.counter}s后跳转`)
.fontSize(15)
.fontColor(Color.Gray)
.id('textJump')
.alignRules({
right: { anchor: '__container__', align: HorizontalAlign.End },
top: { anchor: '__container__', align: VerticalAlign.Top }
})
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.margin({ top: 20, right: 20 })
.borderWidth(1)
.borderRadius(5)
.borderColor(Color.Gray)
.onClick(() => {
this.goIntent()
})
}
.width('100%')
.height('100%')
.onAppear(() => {
LogUtils.info('222222222 显示')
this.intervalID = setInterval(() => {
this.counter = this.counter - 1
if (this.counter == 0) {
clearInterval(this.intervalID)
this.goIntent()
}
}, 1000);
})
.onDisAppear(() => {
LogUtils.info('222222222 隐藏')
this.counter = 5
clearInterval(this.intervalID)
})
}
private goIntent(): void {
let userLoginStatus = false //TODO:获取登录状态,后面实现
if (userLoginStatus) {
router.replaceUrl({ url: 'pages/MainPage' })
} else {
router.replaceUrl({ url: 'pages/LoginPage' })
}
}
}
2、登录页面(LoginPage)
LoginPage页面详细代码:
import { Constants, HttpManager, LoadingDialog, RequestMethod, UserManager } from "@app/BaseLibrary"
import LogUtils from '@app/BaseLibrary/src/main/ets/utils/LogUtils'
import { RegisterBean } from '../bean/RegisterBean'
import promptAction from '@ohos.promptAction'
import { LoginBean } from '../bean/LoginBean'
import router from '@ohos.router'
const TAG = 'LoginPage--- ';
@Entry
@Component
struct LoginPage {
@State username: string = "" //用户名
@State password: string = "" //密码
hideJump: boolean = router.getParams()?.['hideJump']
build() {
RelativeContainer() {
Image($r("app.media.ic_back"))
.width(36)
.height(32)
.id('imageBack')
.margin(20)
.alignRules({
left: { anchor: '__container__', align: HorizontalAlign.Start },
top: { anchor: '__container__', align: VerticalAlign.Top }
})
.onClick(() => {
router.back()
})
if (!this.hideJump) {
Text('跳过')
.fontSize(15)
.fontColor(Color.Gray)
.id('textJump')
.alignRules({
right: { anchor: '__container__', align: HorizontalAlign.End },
top: { anchor: '__container__', align: VerticalAlign.Top }
})
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.margin({ top: 20, right: 20 })
.borderWidth(1)
.borderRadius(5)
.borderColor(Color.Gray)
.onClick(() => {
router.clear()
router.replaceUrl({ url: 'pages/MainPage' })
})
}
Text('登录')
.fontSize(50)
.fontColor(Color.Black)
.fontWeight(FontWeight.Bold)
.id('textLogin')
.margin({ top: 30 })
.alignRules({
middle: { anchor: '__container__', align: HorizontalAlign.Center },
top: { anchor: 'imageBack', align: VerticalAlign.Bottom }
})
//用户名和密码
Column() {
Row() {
Image($r('app.media.username'))
.width(32)
.height(32)
TextInput({ placeholder: '请输入用户名' })
.width(300)
.margin({ left: 16 })
.onChange((value: string) => {
this.username = value
})
}
Row() {
Image($r('app.media.password'))
.width(32)
.height(32)
TextInput({ placeholder: '请输入密码' })
.type(InputType.Password)
.width(300)
.margin({ left: 16 })
.onChange((value: string) => {
this.password = value
})
}.margin({ top: 20 })
}.id('columnLogin')
.margin({ top: 80 })
.alignRules({
middle: { anchor: '__container__', align: HorizontalAlign.Center },
top: { anchor: 'textLogin', align: VerticalAlign.Bottom }
})
Button("注册")
.width(340)
.height(60)
.onClick(() => {
this.registerData()
})
.id('buttonLogin')
.margin({ bottom: 80 })
.alignRules({
middle: { anchor: '__container__', align: HorizontalAlign.Center },
bottom: { anchor: '__container__', align: VerticalAlign.Bottom }
})
Button("登录")
.width(340)
.height(60)
.onClick(() => {
this.dialogController.open()
this.loginData()
})
.id('buttonRegister')
.backgroundColor(Color.Pink)
.margin({ bottom: 40 })
.alignRules({
middle: { anchor: '__container__', align: HorizontalAlign.Center },
bottom: { anchor: 'buttonLogin', align: VerticalAlign.Top }
})
}
.width('100%')
.height('100%')
}
/**
* 登录请求
*/
private loginData(): void {
if (this.username.trim().length == 0 || this.password.trim().length == 0) {
promptAction.showToast({ message: "用户名或密码不能为空" })
return
}
HttpManager.getInstance()
.request<LoginBean>({
method: RequestMethod.POST,
header: { "Content-Type": "application/json" },
url: `https://www.wanandroid.com/user/login?username=${this.username}&password=${this.password}`, //wanAndroid的API:登录
})
.then((result: LoginBean) => {
this.dialogController.close()
LogUtils.info(TAG, "content encodeURIComponent: " + encodeURIComponent(this.username) + " result:" + JSON.stringify(result))
if (result.errorCode == 0) {
AppStorage.SetOrCreate(Constants.APPSTORAGE_ISLOGIN, true)
AppStorage.SetOrCreate(Constants.APPSTORAGE_USERNAME, encodeURIComponent(this.username))
AppStorage.SetOrCreate(Constants.APPSTORAGE_PASSWORD, this.password)
AppStorage.SetOrCreate(Constants.APPSTORAGE_TOKEN_PASS, this.cookiesMatch(JSON.stringify(result.header)))
promptAction.showToast({ message: "登录成功" })
UserManager.setLogIn(true)
router.clear()
router.replaceUrl({ url: 'pages/MainPage' })
} else {
promptAction.showToast({ message: result.errorMsg })
}
})
.catch((error) => {
this.dialogController.close()
promptAction.showToast({ message: "登录失败" })
LogUtils.info(TAG, "error: " + JSON.stringify(error))
})
}
/**
* 注册请求
*/
private registerData(): void {
if (this.username.trim().length == 0 || this.password.trim().length == 0) {
promptAction.showToast({ message: "用户名或密码不能为空" })
return
}
HttpManager.getInstance()
.request<RegisterBean>({
method: RequestMethod.POST,
header: { "Content-Type": "application/json" },
url: `https://www.wanandroid.com/user/register?username=${this.username}&password=${this.password}&repassword=${this.password}`, //wanAndroid的API:注册
})
.then((result: RegisterBean) => {
LogUtils.info(TAG, "result: " + JSON.stringify(result))
if (result.errorCode == 0) {
promptAction.showToast({ message: "注册成功" })
} else {
promptAction.showToast({ message: result.errorMsg })
}
})
.catch((error) => {
promptAction.showToast({ message: "注册失败" })
LogUtils.info(TAG, "error: " + JSON.stringify(error))
})
}
/**
* cookies正则匹配,获取token_pass
* @returns
*/
private cookiesMatch(header: string): string {
LogUtils.info(TAG, " cookiesMatch header: " + header)
let content = header.match(/token_pass_wanandroid_com=(\S*);/)[1]
LogUtils.info(TAG, " cookiesMatch content: " + content)
return content
}
private dialogController = new CustomDialogController({
builder: LoadingDialog({ content: '登录中...' }),
customStyle: true,
alignment: DialogAlignment.Center, // 可设置dialog的对齐方式,设定显示在底部或中间等,默认为底部显示
})
}
登录界面用到的HttpManager网络请求参考之前的文章:鸿蒙自定义Http网络访问组件-CSDN博客
登录成功后通过AppStorage保存登录状态:APPSTORAGE_ISLOGIN,用户名:APPSTORAGE_USERNAME(注:需要执行encodeURIComponent(this.username)进行编码,解决部分特殊用户名乱码问题),密码:APPSTORAGE_PASSWORD,用户登录后的token:APPSTORAGE_TOKEN_PASS(执行 cookiesMatch方法进行正则匹配)
AppStorage.SetOrCreate(Constants.APPSTORAGE_ISLOGIN, true)
AppStorage.SetOrCreate(Constants.APPSTORAGE_USERNAME, encodeURIComponent(this.username))
AppStorage.SetOrCreate(Constants.APPSTORAGE_PASSWORD, this.password)
AppStorage.SetOrCreate(Constants.APPSTORAGE_TOKEN_PASS, this.cookiesMatch(JSON.stringify(result.header)))
3、主页面(MainPage)
MainPage页面详细代码:
import { Constants } from "@app/BaseLibrary"
import LogUtils from "@app/BaseLibrary/src/main/ets/utils/LogUtils"
import { MainTitleBar } from '../widget/MainTitleBar';
@Entry
@Component
struct MainPage {
private tabsController: TabsController = new TabsController();
@State currentTabIndex: number = Constants.HOME_TAB_INDEX;
@State title: string = '首页'
@Builder
TabBuilder(title: string, index: number, normalIcon: Resource, selectIcon: Resource) {
Column() {
Image(this.currentTabIndex == index ? selectIcon : normalIcon)
.width(25)
.height(25)
Text(title)
.margin({ top: 4 })
.fontSize("10fp")
.fontColor(this.currentTabIndex == index ? "#1296db" : "#999999")
}
.justifyContent(FlexAlign.Center)
.height(56)
.width('100%')
.onClick(() => {
this.currentTabIndex = index;
this.tabsController.changeIndex(this.currentTabIndex)
})
}
build() {
Column() {
MainTitleBar({ title: this.title, showSearch: this.currentTabIndex == 0 })
Tabs({
barPosition: BarPosition.End,
controller: this.tabsController,
}) {
// 首页
TabContent() {
// HomePage()
}
.padding({ left: 12, right: 12 })
.tabBar(this.TabBuilder(Constants.HOME_TITLE, Constants.HOME_TAB_INDEX
, $r('app.media.ic_bottom_normal_home'), $r('app.media.ic_bottom_select_home')))
// 项目
TabContent() {
// ProjectPage()
}
.tabBar(this.TabBuilder(Constants.PROJECT_TITLE, Constants.PROJECT_TAB_INDEX
, $r('app.media.ic_bottom_normal_project'), $r('app.media.ic_bottom_select_project')))
// 导航
TabContent() {
// NavigationPage()
}
.padding({ left: 12, right: 12 })
.tabBar(this.TabBuilder(Constants.NAVIGATION_TITLE, Constants.NAVIGATION_TAB_INDEX
, $r('app.media.ic_bottom_normal_navigation'), $r('app.media.ic_bottom_select_navigation')))
//我的
TabContent() {
// MinePage()
}
.tabBar(this.TabBuilder(Constants.MINE_TITLE, Constants.MINE_TAB_INDEX
, $r('app.media.ic_bottom_normal_mine'), $r('app.media.ic_bottom_select_mine')))
}
.width('100%')
.height('100%')
.flexShrink(1)
.scrollable(false)
.animationDuration(100)
.barHeight(56)
.backgroundColor($r('app.color.bar_backgroundColor'))
.barMode(BarMode.Fixed)
.onChange((index: number) => {
this.title = this.getTitle(index)
this.currentTabIndex = index;
})
}.width('100%')
.height('100%')
.backgroundColor(Color.White)
}
private getTitle(index: number): string {
let titleList = [Constants.HOME_TITLE, Constants.PROJECT_TITLE, Constants.NAVIGATION_TITLE, Constants.MINE_TITLE]
LogUtils.info("3333333333333333 titleList: " + titleList[index])
return titleList[index]
}
}
MainTitleBar详细代码:
import router from '@ohos.router'
@Preview
@Component
export struct MainTitleBar {
@Prop title: string
@Prop showSearch: boolean
build() {
RelativeContainer() {
Text(this.title)
.fontSize(24)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
.id("textTitle")
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
Image($r('app.media.ic_search'))
.width(25)
.height(25)
.id("imageSearch")
.margin({ right: 16 })
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
right: { anchor: '__container__', align: HorizontalAlign.End }
})
.onClick(() => {
router.pushUrl({url:'pages/search/SearchPage'})
})
.visibility(this.showSearch ? Visibility.Visible : Visibility.None)
}
.backgroundColor('#1296db')
.width('100%')
.height(56)
}
}
4、界面效果