View Original Article
Recently, I explored writing small Xiaomi Vela quick applications and decided to create a tutorial to summarize and ride the wave of hyperOS's popularity.
This article is aimed at beginners after all, I am one too.
Overview#
Before the introduction, it's important to understand the basic architecture of a Vela app.
Project Structure#
Basic structure:
├── manifest.json
├── app.ux
├── pages
│ ├── index
| | ├── index.ux
| | ├── index.css
| | └── index.js
│ └── detail
| ├── detail.ux
| ├── detail.css
| └── detail.js
├── i18n
| ├── defaults.json
| ├── zh-CN.json
| └── en-US.json
└── common
├── style.css
├── utils.js
└── logo.png
Among them,
-
manifest.json
records the basic information of the appFor example, the following properties are:
package: Package name, your own naming.
name: App name, which will be displayed on the watch.
versionName: Version name, your own naming.
minPlatformVersion: Minimum API version.
icon: Icon file path
{% hideToggle Resource and File Access Rules %}
Resource and File Access Rules
The application resource path is divided into absolute paths and relative paths. A path starting with "/" indicates an absolute path, such as /common/a.png, while a path not starting with "/" is a relative path, such as a.png and ../common/a.png, etc.
Application resource files are divided into code files and resource files. Code files refer to .js/.css/.ux, etc., which contain code, while other files are resource files, which are generally used as data, such as images, videos, etc.
- In code files, when importing other code files, use relative paths, for example: ../common/component.ux;
- In code files, when referencing resource files (such as images, videos, etc.), generally use relative paths, for example: ./abc.png;
- When a code file needs to be imported, if the imported file and the importing file are in the same directory, the imported file can use relative paths when referencing resource files. However, if they are not in the same directory, absolute paths must be used because the imported file will be copied into the importing file during compilation, and the directory structure will change after compilation. For example, if a.css is imported by b.ux, and a.css and b.ux are in the same directory, a.css can reference resource files using relative paths: abc.png. If they are not in the same directory, it must use absolute paths: /common/abc.png. Similarly, when a.ux is imported by b.ux, if a.ux and b.ux are in the same directory, a.ux can reference resource files using relative paths: a.png. If not in the same directory, a.ux must reference resources using absolute paths: /common/abc.png;
- In CSS, consistent with front-end development, use the url(PATH) method to access resource files, such as: url(/common/abc.png).
{% endhideToggle %}
features: Declaration of the interfaces called; some sensitive interfaces can only be used if declared.
designWidth: The size of the design draft, see Page Style and Layout for details.
router: Definition of each page in the app.
{ "package": "com.genkaim.muyu", "name": "Electronic Wooden Fish", "versionName": "1.1.0", "versionCode": 4, "minPlatformVersion": 1000, "icon": "/common/logo.png", "deviceTypeList": [ "watch" ], "features": [ { "name": "system.storage" }, { "name": "system.file" }, { "name": "system.prompt" }, { "name": "system.vibrator" } ], "config": { "logLevel": "log", "designWidth": 600 }, "router": { "entry": "pages/index", "pages": { "pages/index": { "component": "index" }, "pages/settings": { "component": "settings" }, "pages/confirm": { "component": "confirm" }, "pages/about": { "component": "about" } } } }
-
app.ux
contains the basic JS syntax for the app, such as onCreate and onDestroy in the lifecycle.In the code below, it will log "onCreate" when the app opens and "onDestroy" when it exits.
<script> export default { data: { a: 1 }, onCreate() { console.log("onCreate") }, onDestroy() { console.log("onDestroy") } } </script>
-
Each folder under
pages
contains the resource files for the current page, including.ux
, etc. -
i18n
language files. -
common
stores common resource files, such as logos.
Lifecycle#
Here, I will borrow an official diagram.
In simple terms, the app lifecycle involves the following steps when the app is opened. Before the page onShow, it first goes through the onInit and onReady stages (these are the specific two functions in export default).
When the app exits, it triggers onDestroy.
Basic Understanding of ux
Files#
Below is the content of a simple .ux
file:
<template>
<div class="page">
<text class="title">Are you sure you want to clear all merits?</text>
<input class="yes" type="button" value="Confirm" onclick="clearData" />
<input class="no" type="button" value="Cancel" onclick="goBack" />
</div>
</template>
<script>
import prompt from '@system.prompt'
export default {
goBack(event) {
router.back()
},
clearData(event) {
router.back()
}
}
</script>
<style>
.yes, .no {
width: 500px;
height: 150px;
color: white;
font-size: 40px;
font-weight: bold;
margin-top: 60px;
}
.page {
position: absolute;
background-color: black;
display: flex;
flex-direction: column;
display: flex;
justify-content: center; /* Center horizontally */
align-items: center; /* Center vertically */
}
.back {
position: absolute;
left: 0px;
top: 10px;
background-color: black;
color: white;
font-size: 80px;
font-weight: bold;
background-image: url('/common/icon-back.png');
}
.yes {
margin-top: 50px;
background-color: rgb(44, 44, 44);
}
.no {
margin-top: 30px;
}
.title {
color: white;
font-size: 50px;
font-weight: bold;
top: 5px;
}
</style>
- template tag: Similar to HTML language, Vela provides various components. For example, two different components appear here:
text and input, which will be rendered as a text block and a button, respectively.
Among them:
1.1 class is the user-defined class name, and type is the component type, which is a button here.
1.2 The value of the button tag is the text displayed on the button.
1.3 onclick is the event binding, meaning that when the "tap" condition is met, the specified function will be called, and the function is defined in export default.
Why wrap another layer of div under template?
Because only one root node is allowed under template, so a node must be created first, and then parallel nodes can be placed inside.
- script tag: Supports ES5 / ES6 syntax. In the export default here, a module is imported to implement the return functionality.
- style cascading style sheet: Selectors can be used to choose and set properties.
Simple Example: Basic Functionality Implementation of Electronic Wooden Fish#
Basic Functionality:#
-
Page elements: Electronic wooden fish (image), display merit value functionality, "Merit +1" animation.
-
Implement tap counting functionality on the wooden fish.
Page resource: Click to Download
Page Layout#
- The wooden fish image can be placed using the img component or CSS; here, the former is used.
Because the tap feedback functionality needs to be implemented, the onclick event needs to be bound.
<img class="muyu" src="muyu.png" onclick="rpPlusPlus()" />
- The counting functionality can be displayed using the text component. Note that dynamic data (variables) can be displayed using curly braces, for example, the following code can display the merit count (the variable is defined below).
<text class="cnt" >Merit {{localCnt}}</text>
- The animation is implemented through the text component + if condition rendering + binding animation (defined below).
If condition rendering means that when the value of the expression inside if is true, the current element will be rendered.
<text if="{{plus}}" class="showPlus">Merit +1</text>
Script#
- Data processing:
In the above, several variables are needed: localCnt(int) for counting merits, plus(bool) for indicating message display.
Defining variables should be written as follows:
Among them,
Property | Type | Description |
---|---|---|
public | Object | Page-level component data model, affects the data overriding mechanism: properties defined in public allow incoming data to override them. If an external data property is not declared, it will not be added in public. |
protected | Object | Page-level component data model, affects the data overriding mechanism: properties defined in protected allow data passed from internal pages of the application to override them, but do not allow data from external requests to override them. |
private | Object | Page-level component data model, affects the data overriding mechanism: properties defined in private cannot be overridden. |
<script>
export default {
public: {
localCnt: 0,
plus: false
}
}
</script>
- In the above, the rpPlusPlus function needs to be defined to implement merit +1.
Among them:
You can access the current page data object through this.Name.
And set plus to true, then set it to false after 400ms, which makes it display for 400ms, as it is bound to an animation (defined below), making it appear that the text moves up and then disappears.
rpPlusPlus(event) {
this.plus = true;
this.localCnt++;
setTimeout(() => {
this.plus = false;
}, 400);
}
CSS#
- Binding animation:
The class showPlus has already been defined above, so select the element through .showPlus
:
.showPlus {
position: absolute;/*positioning*/
right: 70px;
top: 210px;
color: white;/*font color*/
font-weight: bold;/*font weight*/
animation-name: moveUp;/*bind animation to text*/
animation-delay: 0s;/*animation delay time*/
animation-duration: 200ms;/*animation duration*/
animation-iteration-count: 1;/*animation repeat count*/
}
@keyframes moveUp {/*define animation*/
0% {
transform: translateY(0);
}
99% {
transform: translateY(-40px);
}
100% {
visibility: hidden;
}
}
- cnt, page, and muyu elements:
.muyu {
position: absolute;/*positioning*/
height: 350px;
top: 250px;
background-color: transparent;/*background color*/
width: 450px;/*size*/
}
.cnt {
position: absolute;
bottom: 100px;/*positioning*/
font-size: 70px;/*font size*/
color: white;/*font color*/
}
.page {
background-color: black;/*background color*/
display: flex;
flex-direction: column;
justify-content: center; /* Center horizontally */
align-items: center; /* Center vertically */
height: 100%; /* Ensure the container fills the entire viewport */
}
Complete code:
<template>
<div class="page">
<img class="muyu" src="muyu.png" onclick="rpPlusPlus()" />
<text if="{{plus}}" class="showPlus">Merit +1</text>
<text class="cnt" >Merit {{localCnt}}</text>
</div>
</template>
<script>
export default {
public: {
localCnt: 0,
plus: false
},
rpPlusPlus(event) {
this.plus = true;
this.localCnt++;
setTimeout(() => {
this.plus = false;
}, 400);
}
}
</script>
<style>
.page {
background-color: black;/*background color*/
display: flex;
flex-direction: column;
justify-content: center; /* Center horizontally */
align-items: center; /* Center vertically */
height: 100%; /* Ensure the container fills the entire viewport */
}
.showPlus {
position: absolute;/*positioning*/
right: 70px;
top: 210px;
color: white;/*font color*/
font-weight: bold;/*font weight*/
animation-name: moveUp;/*bind animation to text*/
animation-delay: 0s;/*animation delay time*/
animation-duration: 200ms;/*animation duration*/
animation-iteration-count: 1;/*animation repeat count*/
}
@keyframes moveUp {/*define animation*/
0% {
transform: translateY(0);
}
99% {
transform: translateY(-40px);
}
100% {
visibility: hidden;
}
}
.muyu {
position: absolute;/*positioning*/
height: 350px;
top: 250px;
background-color: transparent;/*background color*/
width: 450px;/*size*/
}
.cnt {
font-size: 70px;/*font size*/
bottom: 100px;/*positioning*/
position: absolute;
color: white;
}
</style>
If there are any errors, please feel free to point them out.