They're custom components!,...are you telling me...you've never heared of them..!?
Hi, my name's Younes Megaache, and in this tutorial, I will teach you about custom component on harmonyOS. or at least I wll try LOL!
Am I doing this right? The first sentence is always the hardest!.
We will be creating creating two (2) custom components, a compound component and a custmized one. After reading this article and checking the demo code, you should have learned enough to start creating you're own custome components, or may be not XD!.
You can find links below with more details on this topic.
The code is uploaded to github, the link can be found at the end of the article (bottom).
Table of content
Overview
Create component 1: SButton (compound component)
Define custom component class.
Define custom component layout.
Add component to layout.
Define custom attributes.
Apply custom attributes.
Finishing the components.
Create component 2: ShapeShiftingComponent
Define custom component class.
Add component to layout.
Define custom attributes.
Apply custom attributes.
Drawing a shape
Toggling between shapes
Finishing the component
Tips and tricks
Libraries
Github project (source code)
References
OverviewHarmonyOS UI elements are all based on Component (single element on screen) and ComponentContainer (collection of elements on screen). There are many "widgets" and "layouts" built-in that can be used to build the UI such as components like Button and Text, and component containers like DirectionalLayout and StackLayout.
In some apps though we need to be able to customize components to suit our own needs. This might mean extending an existing component or creating your own Component subclass.
Customizing your own components involves extending Component or an existing subclass, overriding the component behavior by writing methods such as onDraw or onTouchEvent and then using your new component in an ability.
Creating custom components is centered around 4 primary aspects that we may need to control or modify:
Drawing: Control the rendering of the component on screen visually by adding draw tasks and overriding the onDraw method.
Interaction: Control the ways the user can interact with the component with the onTouchEvent and gestures.
Measurement: Control the content dimensions of the component on screen by overriding the onEstimateSize method.
Attributes: Defining custom XML attributes for your component and using them to control behavior with TypedArray
To take a closer look, we will create two components:
first, we will create a compound component where we will be putting together multiple components as one
second, suppose we want to create our own component control that allows the user to select between different shapes. The component will display a single shape (rectangle, circle or triangle) and clicking on the component will toggle the shape selected between the different options.
Before we start: some resources like colors, strings, backgrounds are not included in this article, visit the github project for full code
Component 1: SButton (compound component) The first component that we'll be creating is a compound button, which will consist of an icon, text and an arrow, This will help us understand creating a reusable component by putting together multiple components .
Define Custom component ClassTo create our own custom component, we start by defining a SButton which extends from StackLayout, as it's a ComponentContainer that will host multiple components. then implement the required constructor:
Java:
public class SButton extends StackLayout {
public SButton(Context context, AttrSet attrSet) {
super(context, attrSet);
}
}
Define custom component layoutCreate a layout file component_sbutton.xml, which will hold 3 components as described earlier:
XML:
<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:height="match_content"
ohos:width="match_parent"
ohos:alignment="center"
ohos:orientation="horizontal"
ohos:padding="3vp"
>
<DirectionalLayout
ohos:id="$+id:button_root"
ohos:height="match_content"
ohos:width="match_parent"
ohos:alignment="center"
ohos:background_element="$graphic:bg_button_red"
ohos:orientation="horizontal"
>
<DirectionalLayout
ohos:height="match_content"
ohos:width="match_parent"
ohos:alignment="center"
ohos:bottom_margin="3vp"
ohos:left_margin="10vp"
ohos:min_height="40vp"
ohos:orientation="horizontal"
ohos:right_margin="10vp"
ohos:top_margin="3vp"
>
<Image
ohos:id="$+id:button_icon"
ohos:height="30vp"
ohos:width="28vp"
ohos:clip_alignment="center"
ohos:image_src="$media:icon"
ohos:layout_alignment="center"
ohos:scale_mode="clip_center"
ohos:visibility="visible"
ohos:weight="1.2"
/>
<Text
ohos:id="$+id:button_text"
ohos:height="match_content"
ohos:width="match_content"
ohos:left_margin="5vp"
ohos:multiple_lines="true"
ohos:text="Button"
ohos:text_alignment="center"
ohos:text_color="white"
ohos:text_size="18fp"
ohos:text_weight="700"
ohos:weight="5"
/>
<Text
ohos:id="$+id:button_arrow"
ohos:height="match_parent"
ohos:width="match_content"
ohos:left_margin="10vp"
ohos:text="→"
ohos:text_color="white"
ohos:text_size="20fp"
ohos:text_weight="800"
ohos:visibility="visible"
ohos:weight="1"
/>
</DirectionalLayout>
</DirectionalLayout>
</DirectionalLayout>
The component has a default red background defined in graphic/bg_button_red.xml:
XML:
<?xml version="1.0" encoding="UTF-8" ?>
<shape xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:shape="rectangle">
<solid
ohos:color="$color:red"/>
<corners
ohos:radius="50"/>
</shape>
In the previewer you should see this:
Let's set set the layout and initialize the components before we move to the attributes :
Java:
public class SButton extends StackLayout {
//...
/**
* root layout, parent of other components {@link com.megaache.customcomponent.ui.components.SButton#textC} {@link com.megaache.customcomponent.ui.components.SButton#iconC} {@link com.megaache.customcomponent.ui.components.SButton#arrowC}
*/
private DirectionalLayout root;
/**
* button text, shown in the center of the button
*/
private Text textC;
/**
* icon component shown on the left side of the button's text
*/
private Image iconC;
/**
* arrow component shown on the rigth side of button's text
*/
private Text arrowC;
public SButton(Context context, AttrSet attrSet) {
super(context, attrSet);
LayoutScatter.getInstance(getContext())
.parse(ResourceTable.Layout_component_sbutton, this, true);
root = (DirectionalLayout) findComponentById(ResourceTable.Id_button_root);
textC = (Text) findComponentById(ResourceTable.Id_button_text);
arrowC = (Text) findComponentById(ResourceTable.Id_button_arrow);
iconC = (Image) findComponentById(ResourceTable.Id_button_icon);
}
}
Add Component to LayoutNext, let's add this component to our ability layout:
Code:
<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
xmlns:ohos="http://schemas.huawei.com/res/ohos"
xmlns:sbutton="http://huawei.com/res/ohos"
ohos:height="match_parent"
ohos:width="match_parent"
ohos:background_element="black"
ohos:orientation="vertical"
ohos:padding="20vp"
>
<com.megaache.customcomponent.ui.components.SButton
ohos:id="$+id:sbutton"
ohos:height="match_content"
ohos:width="200vp"
ohos:layout_alignment="horizontal_center"
/>
</DirectionalLayout>
Note: how we define a custom namespace SButton. This namespace allows you to allow Harmony to auto-resolve the namespace, avoiding the annoying IDE errors
this is the component in its default state:
Define Custom Attributes
Well-written custom components can be configured and styled via XML attributes. You need to ask yourself which aspects of your component should be customizable. For example, we might want to let the user set the background (color) of the button, set icon source, set the button's text as well as give the developer the option to hide or show an arrow icon. We might want the component to be configurable in XML as follows:
XML:
<com.megaache.customcomponent.ui.components.SButton
ohos:id="$+id:sbutton"
ohos:height="match_content"
ohos:width="200vp"
ohos:layout_alignment="horizontal_center"
sbutton:btn_bg="$graphic:button_violet"
sbutton:icon_src="$media:phone"
sbutton:show_arrow="true"
sbutton:text="$string:hello"
/>
Once you define the custom attributes, you can use them in layout XML files, You won't get similar experience like built-in attributes (autocomplete) and that's because DevEco studio does not support defining the attribute format yet!.
A difference between custom attributes and built-in attribute is that your custom attributes belong to a different namespace. You can define the namespace within the root component of the layout and configure the properties for the component. You would need to use the namespace http://huawei.com/res/ohos:
XML:
<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
xmlns:ohos="http://schemas.huawei.com/res/ohos"
xmlns:sbutton="http://huawei.com/res/ohos"
ohos:height="match_parent"
ohos:width="match_parent"
ohos:background_element="black"
ohos:orientation="vertical"
ohos:padding="20vp"
>
<com.megaache.customcomponent.ui.components.SButton
ohos:id="$+id:sbutton"
ohos:height="match_content"
ohos:width="200vp"
ohos:layout_alignment="horizontal_center"
sbutton:btn_bg="$graphic:button_violet"
sbutton:icon_src="$media:phone"
sbutton:show_arrow="true"
sbutton:text="$string:hello"
/>
</DirectionalLayout>
Apply Custom AttributesNow that we have set custom properties such as btn_bg, icon_src, show_arrow and text, we need to extract those properties to be used within our custom component within the constructor, from AttrSet parameter:
First, let's created an inner class that will hold the attribute name, to keep our code clean:
Java:
public class SButton extends StackLayout {
public static class SButtonAttrsConstants {
//if set to true, arrow will be shown on right side of the text
public static final String SHOW_ARROW = "show_arrow";
//if set, icon will be shown on left side of text
public static final String ICON_SRC = "icon_src";
//text to be shown in the button
public static final String BTN_TEXT = "text";
//background for the container
public static final String BTN_BG = "btn_bg";
}
//...
}
The attributes are passed to the custom component inside AttrSet parameter, We are gonna use the method ifPresent when extracing the attribute for safety:
Java:
attrSet.getAttr("attribute_name").ifPresent(attr -> attr.getStringValue() )
There are seven (7) types of attribute, make sure to use the right method to get the attribute value according to its format:
String: ex app:text="blah blah", app:text="$string:blah_blah"
Code:
attr.getStringValue();
Integer, ex app:number="1"
Code:
attr.getIntegerValue();
graphic or media, ex app:graphic="$graphic:bg_red", app:icon="$media:banana"
Code:
attr.getElement();
Boolean, ex app:hide="true"
Code:
attr.getBoolValue();
Dimension, ex app:dimen="2vp"
Code:
attr.getDimensionValue();
Float, ex app:float="22.34"
Code:
attr.getFloatValue();
Long, ex app:long="1234543212345"
Code:
attr.getLongValue();
extracting the attribute values:
Code:
public class SButton extends StackLayout {
//...
public SButton(Context context, AttrSet attrSet) {
super(context, attrSet);
//...
if (attrSet != null) {
attrSet.getAttr(SButtonAttrsConstants.SHOW_ARROW).ifPresent(attr -> setShowArrow(attr.getBoolValue()));
attrSet.getAttr(SButtonAttrsConstants.BTN_TEXT).ifPresent(attr -> setText(attr.getStringValue()));
attrSet.getAttr(SButtonAttrsConstants.BTN_TEXT).ifPresent(attr -> setText(attr.getIntegerValue()));
attrSet.getAttr(SButtonAttrsConstants.ICON_SRC).ifPresent(attr -> setIconRes(attr.getElement()));
attrSet.getAttr(SButtonAttrsConstants.BTN_BG).ifPresent(attr -> setBg(attr.getElement()));
}
}
//...
}
then we write some logic to update the component based on the attributes:
Java:
public class SButton extends StackLayout {
//...
/**
* set button text
*
* @param textRes resource id of text that will be shown inside button
*/
public void setText(int textRes) {
if (textRes != -1) {
this.textC.setText(textRes);
}
}
/**
* show icon on left side of the text
*
* @param imageElement image element ex: {@link ResourceTable#Media_phone}
* if imageElement is null, the icon component will be hidden
*/
public void setIconRes(Element imageElement) {
if (imageElement == null) {
UiUtils.hide(iconC);
} else {
imageElement.setBounds(0, 0, iconC.getRight(), iconC.getBottom());
iconC.setImageElement(imageElement);
UiUtils.visible(iconC);
}
}
/**
* set button text
*
* @param text will be shown inside button
*/
public void setText(String text) {
if (text != null && !text.isEmpty()) {
this.textC.setText(text);
}
}
/**
* show arrow on right side of the text
*
* @param showArrow true to show an arrow, false otherwise
*/
public void setShowArrow(boolean showArrow) {
if (showArrow)
UiUtils.visible(arrowC);
else
UiUtils.hide(arrowC);
}
/**
* change button background
*
* @param shapeElement element that will be set as background
*/
public void setBg(Element shapeElement) {
root.setBackground(shapeElement);
}
}
Finishing the ComponentFinally, lets add some animation to our custom component which is totally optional:
Java:
public class SButton extends StackLayout {
//...
@Override
public void setClickedListener(ClickedListener listener) {
super.setClickedListener((c) -> {
animateTouchDown();
getContext().getUITaskDispatcher().delayDispatch(() -> {
animateTouchUp();
listener.onClick(c);
}, 300);
});
}
/**
* scale button up when clicked, called when the button is clicked
*/
private void animateTouchDown() {
setScale(1.1f, 1.1f);
setAlpha(0.5f);
}
/**
* scale button down to normal size, called when the button is clicked
*/
private void animateTouchUp() {
setScale(1, 1);
setAlpha(1f);
}
}
then within the ability, we can display a toast when the button is clicked:
Java:
public class MainAbility extends Ability {
@Override
public void onStart(Intent intent) {
super.onStart(intent);
setUIContent(ResourceTable.Layout_ability_main);
SButton sButton = (SButton) findComponentById(ResourceTable.Id_sbutton);
sButton.setClickedListener(c -> toast("you clicked the SButton!"));
}
private void toast(String msg) {
new ToastDialog(this).setText(msg).setAlignment(LayoutAlignment.CENTER).show();
}
}
Run the app:
Component 2: ShapeShiftingButtonThe second component that we will be creating is a fully custom “shape shifting”‘component that changes it’s shape when it’s clicked.
Define Custom Component ClassWe start by defining a ShapeShiftingComponent which extends from Component and implements the required constructor:
Java:
public class ShapeShiftingComponent extends Component {
public ShapeShiftingComponent(Context context, AttrSet attrSet) {
super(context, attrSet);
}
}
Add Component to LayoutNext, let's add this component to our ability layout:
XML:
<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
xmlns:ohos="http://schemas.huawei.com/res/ohos"
xmlns:app="http://huawei.com/res/ohos"
ohos:height="match_parent"
ohos:width="match_parent"
ohos:background_element="black"
ohos:orientation="vertical"
ohos:padding="20vp"
>
<com.megaache.customcomponent.ui.components.ShapeShiftingComponent
ohos:id="$+id:ssbutton"
ohos:height="80vp"
ohos:width="160vp"
ohos:layout_alignment="horizontal_center"
ohos:text_color="white"
ohos:text_size="18fp"
ohos:top_margin="20vp"
/>
</DirectionalLayout>
Define Custom Attributesfor this component, we can let the user set the initial shape which can be done as follow:
XML:
<com.megaache.customcomponent.ui.components.ShapeShiftingComponent
ohos:id="$+id:ssbutton"
ohos:height="80vp"
ohos:width="160vp"
ohos:layout_alignment="horizontal_center"
ohos:text_color="white"
ohos:text_size="18fp"
ohos:top_margin="20vp"
app:shape="rectangle"
app:shape_color="#ff0000"
/>
Apply Custom AttributesWe have set custom attribute shape and shape_color lets extract this attribute from AttrSet parameter.
Lets declare the attribute name in separate class:
Java:
public class ShapeShiftingComponent extends Component {
public static class CSButtonAttrsConstants {
public static final String CSBUTTON_SHAPE = "shape";
public static final String CSBUTTON_SHAPE_COLOR = "shape_color";
}
}
Lets extract the values of attributes which will be used within the constructor of our component:
Java:
public class ShapeShiftingComponent extends Component {
//...
public static class CSButtonAttrsConstants {
public static final String CSBUTTON_SHAPE = "shape";
public static final String CSBUTTON_SHAPE_COLOR = "shape_color";
}
/**
* button shape, can be set inside layout file, ex: app:shape="rectangle"
* must be one of: rectangle, circle, triangle
*/
private String shape;
/**
* the color of the shape, can be set using attribute inside layout (xml) file
*/
private Color shapeColor;
public ShapeShiftingComponent(Context context, AttrSet attrSet) {
super(context, attrSet);
if (attrSet != null) {
attrSet.getAttr(CSButtonAttrsConstants.CSBUTTON_SHAPE)
.ifPresent(attr -> shape = attr.getStringValue());
attrSet.getAttr(CSButtonAttrsConstants.CSBUTTON_SHAPE_COLOR)
.ifPresent(attr -> shapeColor = attr.getColorValue());
}
}
//...
}
Drawing a shapeNext, let's actually draw a rectangle taking into account shape color. we will use addDrawTask to draw the shape for now, in future releases of HarmonyOs SDK, the components will have method onDraw where you can draw shapes:
Java:
public class ShapeShiftingComponent extends Component {
//...
/**
* the color of the shape, can be set using attribute inside layout (xml) file
*/
private Color shapeColor;
/**
* WIDTH of the button, used to draw shape using canvas in {@link ShapeShiftingComponent#drawShape}
*/
public static final int WIDTH = 160;
/**
* HEIGHT of the button, used to draw shape using canvas in {@link ShapeShiftingComponent#drawShape}
*/
public static final int HEIGHT = 80;
/**
* the y offset of the text that's shown inside the component (button)
*/
public static final int TEXT_Y_OFFSET = HEIGHT / 2 + 5;
/**
* size of the text that's shown inside the component (button)
*/
public static final int TEXT_SIZE_FP = 20;
/**
* paint used to draw the shapes, defines the color of the shape
* used in {@link ShapeShiftingComponent#drawShape()}
*/
private Paint shapePaint;
/**
* paint used to draw the text (shape name), it defines the color and font size of the text
* used in {@link ShapeShiftingComponent}
*/
private Paint textPaint;
public ShapeShiftingComponent(Context context, AttrSet attrSet) {
super(context, attrSet);
//...
preparePaint();
drawShape();
}
/**
* add draw task to draw shape and it's name (shape)
*/
private void drawShape() {
addDrawTask((component, canvas) -> {
//draw rectangle
canvas.drawRect(0, 0, toPx(WIDTH), toPx(HEIGHT), shapePaint);
//draw text 'rectangle' inside shape
canvas.drawText(textPaint, shape, toPx(WIDTH / 2 - (9 * shape.length() / 2)), toPx(TEXT_Y_OFFSET));
});
}
/**
* convert VP (visual/density pixel) to pixels
*
* @param vp value in VP (density pixel)
* @return value in pixels
*/
private float toPx(int vp) {
return AttrHelper.vp2px(vp, getContext());
}
/**
* initial paint objects, that are used to draw the shape and text (shape name)
*/
private void preparePaint() {
shapePaint = new Paint();
shapePaint.setStyle(Paint.Style.FILL_STYLE);
shapePaint.setColor(shapeColor);
textPaint = new Paint();
textPaint.setStyle(Paint.Style.FILL_STYLE);
textPaint.setColor(Color.WHITE);
textPaint.setTextSize(AttrHelper.fp2px(TEXT_SIZE_FP, getContext()));
}
}
this will draw the shape based on the attributes shape and shape_color defined in XML layout file:
you can set the attribute shape to circle:
or triangle:
Toggling between shapesNow, to make our custom component stand to its name, we have to make it change shape each time it's clicked.
Let's setup a "clicked listener" to invalidate the layout to force re-draw the component:
Java:
public class ShapeShiftingComponent extends Component {
//...
public static final String CIRCLE = "circle";
public static final String RECTANGLE = "rectangle";
public static final String TRIANGLE = "triangle";
private final String[] shapes = new String[]{RECTANGLE, TRIANGLE, CIRCLE};
/**
* button shape, can one of {@link ShapeShiftingComponent#shapes}
*/
private String shape;
/**
* used to loop through the array of shapes {@link ShapeShiftingComponent#shapes}
*/
private int shapeIndex;
public ShapeShiftingComponent(Context context, AttrSet attrSet) {
super(context, attrSet);
//...
registerListener();
}
/**
* register click listener to change shape everytime the component is clicked
*/
private void registerListener() {
this.setClickedListener(c -> {
shapeIndex++;
shape = shapes[shapeIndex % shapes.length];
invalidate();
postLayout();
});
}
//...
}
Now whenever the shape is clicked, the selected shape is changed and the OS should call our drawing task once again.
Let's update our draw task to handle drawing other shapes (circle, triangle):
Java:
public class ShapeShiftingComponent extends Component {
//...
public static final int CIRCLE_RADIUS = 120;
/**
* add draw task to draw the next shape of the component, and it's name (shape)
* also show toast for the current shape
*/
private void drawShape() {
addDrawTask((component, canvas) -> {
new ToastDialog(getContext())
.setAlignment(LayoutAlignment.CENTER)
.setText("shape:" + shape).show();
switch (shape) {
case CIRCLE: {
Point center = new Point(WIDTH * 1.5f, HEIGHT * 1.5f);
canvas.drawCircle(center, CIRCLE_RADIUS, shapePaint);
break;
}
case TRIANGLE: {
Path trianglePath = new Path();
trianglePath.moveTo(0, HEIGHT * 2); //bottom left
trianglePath.lineTo(WIDTH * 3, HEIGHT * 2); //bottom right
trianglePath.lineTo(WIDTH * 1.5f, 0); //center
canvas.drawPath(trianglePath, shapePaint);
break;
}
default: {
canvas.drawRect(0, 0, toPx(WIDTH), toPx(HEIGHT), shapePaint);
}
}
canvas.drawText(textPaint, shape, toPx(WIDTH / 2 - (9 * shape.length() / 2)), toPx(TEXT_Y_OFFSET));
});
}
//...
}
Finishing the componentThe final touch is to add the default values: or may be this should have been the first step LOL!
Java:
public class ShapeShiftingComponent extends Component {
//...
public ShapeShiftingComponent(Context context, AttrSet attrSet) {
super(context, attrSet);
setDefaultValues();
//...
}
/**
* set the default values, in case the developer didn't specify the values in layout file (xml)
*/
private void setDefaultValues() {
shape = RECTANGLE;
shapeColor = new Color(getContext().getColor(ResourceTable.Color_ssbutton_default));
shapeIndex = 0;
}
//...
}
Run the app, click the component to loop throught the shapes:
Tips and tricks
There are many other things to cosider when creating custom component, like measurements or touch events, check out the custom component guide on the official website.
If you have an existing custome component that's similar to what you want to create, you can simlply extend that custom component and just override the behavior that you want and get the rest for free
You can also create a custom layout by extending ComponetContainer, to act as a container for your other components
To run the project you're advised to HVD manager to test on a remote device, If you want to run the project on a physical device then you have to configure the signin config (last link in references)
Librariesthere are some libraries for harmonyOs that have custom comopnents, you can check some picks below. the libraries are open source so you may wanna have a look at the source code to learn more:
BadgeView: https://gitee.com/openharmony-tpc/BadgeView
CircleImageView: https://gitee.com/openharmony-tpc/CircleImageView
ArcProgresssStackView: https://gitee.com/openharmony-tpc/ArcProgressStackView
Github project Click here
References
https://developer.harmonyos.com/en/docs/documentation/doc-guides/ui-java-custom-components-0000001139369661
https://developer.harmonyos.com/en/docs/documentation/doc-guides/ui-java-custom-layouts-0000001092683918
https://developer.harmonyos.com/en/docs/documentation/doc-references/component-0000001054678683
https://developer.harmonyos.com/en/docs/documentation/doc-guides/build_hap-0000001053342418
sorry If the links are not clickable, it's not on me
Original Source
Thanks for reading!
How to set the size and color of button inside custom class which extends Stack Layout?
Related
This article is originally from HUAWEI Developer Forum
Forum link: https://forums.developer.huawei.com/forumPortal/en/home
HiAi Image Super Resolution
Upscales an image or reduces image noise and improves image details without changing the resolution.
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Upscales an image or reduces image noise and improves image details without changing the resolution.
Base on AI deep learning of CV (Computer Vision)
Utilize Huawei NPU (Neural Processing Unit), 50X faster than CPU
1X & 3X super-resolution furnish images with clearer effect, reducing JPEG compression noise
You can check the offical documentation about HiAi Image super resolution.
Huawei continuous investment on NPU technology
Huawei Phones support HiAI
Software: Huawei EMUI 9.0 & above
Hardware: CPU 970,810, 820,985,990
Codelab
https://developer.huawei.com/consumer/en/codelab/HiAIImageSuperresolution/index.html#0
You can also follow the codelab to implement the HiAi image resolution with the help DevEco IDE plugin in Android Studio.
Project: (HiAi Image Super Resolution)
In this article we are going to make project in which we can implement HiAi Image Super Resolution to improve low resolution image quality which is used in most of the application as a thumbnail images.
1. Implementation:
Download the vision-oversea-release.aar package in the Huawei AI Engine SDKs from the Huawei developer community.
Copy the downloaded vision-oversea-release.aar package to the app/libs directory of the project.
Add the following code to build.gradle in the APP directory of the project, and add vision-release.aar to the project. Dependency on the Gson library must be added, because the conversion of parameters and results between the JavaScript Object Notation (JSON) and Java classes inside vision-release.aar depends on the Gson library.
Code:
repositories {
flatDir {
dirs 'libs'
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation(name: 'vision-oversea-release', ext: 'aar')
implementation 'com.google.code.gson:gson:2.8.6'
}
2. Assets:
In this section we adding some low resolution images in "assets/material/image_super_resolution" directory, to further fetch images for local direction for optimization.
3. Design ListView:
In this section we are design ListView in our layouts to show original images and optimized images.
activity_main.xml
Code:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="1pt"
android:layout_marginBottom="5pt"
android:orientation="horizontal"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="Original Image"
android:textSize="24sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="Improved Image"
android:textSize="24sp" />
</LinearLayout>
<ListView
android:id="@+id/item_listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:dividerHeight="3pt"
/>
</LinearLayout>
items.xml
Code:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<ImageView
android:id="@+id/imgOriginal"
android:layout_width="100dp"
android:layout_height="100dp"
app:srcCompat="@drawable/noimage"
android:layout_gravity="start"
android:layout_weight="1"
/>
<TextView
android:id="@+id/imgTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=" "
android:layout_gravity="center_horizontal"
android:layout_weight="0"
android:textAlignment="center"
/>
<ImageView
android:id="@+id/imgConverted"
android:layout_width="100dp"
android:layout_height="100dp"
app:srcCompat="@drawable/noimage"
android:layout_gravity="end"
android:layout_weight="1"
app:layout_constraintDimensionRatio="h,4:3"
/>
</LinearLayout>
4. Coding: (Adapter, HiAi Image Super Resolution )
Util Class:
We make a util class AssetsFileUtil, to get all the images from local assets directory, get single BitMap image.
Code:
public class AssetsFileUtil {
public static Bitmap getBitmapByFilePath(Context context, String filePath){
try{
AssetManager assetManager = context.getAssets();
InputStream is = assetManager.open(filePath);
Bitmap bitmap = BitmapFactory.decodeStream(is);
return bitmap;
}catch (Exception e){
e.printStackTrace();
return null;
}
}
public static List<Bitmap> getBitmapListByDirPath(Context context, String dirPath){
List<Bitmap> list = new ArrayList<Bitmap>();
try{
AssetManager assetManager = context.getResources().getAssets();
String[] photos = assetManager.list(dirPath);
for(String photo : photos){
if(isFile(photo)){
Bitmap bitmap = getBitmapByFilePath(context,dirPath + "/" + photo);
list.add(bitmap);
}else {
List<Bitmap> childList = getBitmapListByDirPath(context,dirPath + "/" + photo);
list.addAll(childList);
}
}
}catch (Exception e){
e.printStackTrace();
}
return list;
}
public static List<String> getFileNameListByDirPath(Context context, String dirPath){
List<String> list = new ArrayList<String>();
try{
AssetManager assetManager = context.getResources().getAssets();
String[] photos = assetManager.list(dirPath);
for(String photo : photos){
if(isFile(photo)){
list.add(dirPath + "/" + photo);
}else {
List<String> childList = getFileNameListByDirPath(context,dirPath + "/" + photo);
list.addAll(childList);
}
}
}catch (Exception e){
e.printStackTrace();
}
return list;
}
public static boolean isFile(String fileName){
if(fileName.contains(".")){
return true;
}else {
return false;
}
}
}
MainActivity Class:
In this class we are getting local images and attaching images list to our Adapter
Code:
public class MainActivity extends AppCompatActivity {
private String mDirPath;
private ArrayList<Item> itemList;
private List<String> imageList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
itemList = new ArrayList<Item>();
getLocalImages();
// Setting Adapter and listview
ItemAdapter itemAdapter = new ItemAdapter(getApplicationContext(), R.layout.items, itemList);
ListView listView = findViewById(R.id.item_listView);
listView.setAdapter(itemAdapter);
}
public void getLocalImages(){
mDirPath ="material/image_super_resolution";
imageList = AssetsFileUtil.getFileNameListByDirPath(this,mDirPath);
for(int i=0; i<imageList.size();i++){
itemList.add(new Item(imageList.get(i), " ", imageList.get(i)));
}
}
}
Item Class:
Prepare item data class.
Code:
public class Item {
private String imgOriginal;
private String imgTitle;
private String imgConverted;
public Item(String imgOriginal, String imgTitle, String imgConverted) {
this.imgOriginal = imgOriginal;
this.imgTitle = imgTitle;
this.imgConverted = imgConverted;
}
public String getImgOriginal() {
return imgOriginal;
}
public void setImgOriginal(String imgOriginal) {
this.imgOriginal = imgOriginal;
}
public String getImgTitle() {
return imgTitle;
}
public void setImgTitle(String imgTitle) {
this.imgTitle = imgTitle;
}
public String getImgConverted() {
return imgConverted;
}
public void setImgConverted(String imgConverted) {
this.imgConverted = imgConverted;
}
}
ItemAdapter Class:
In this class we are binding our images array list with our layout ImageView and implement HiAi Image Resolution to optimize image resolution.
ItemAdapter class extends ArrayAdapter with the type of Item class.
Code:
public class ItemAdapter extends ArrayAdapter<Item>
Define some constants
Code:
private ArrayList<Item> itemList;
private final static int SUPERRESOLUTION_RESULT = 110;
private Bitmap bitmapOriginal;
private Bitmap bitmapConverted;
ImageView imgOriginal;
ImageView imgConverted;
private String TAG = "ItemAdapter";
Prepare constructor for the Adpater class
Code:
public ItemAdapter(@NonNull Context context, int resource, @NonNull ArrayList<Item> itemList) {
super(context, resource, itemList);
this.itemList = itemList;
}
Define initHiAi function to check service connected or disconnected
Code:
/**
* init HiAI interface
*/
private void initHiAI() {
/** Initialize with the VisionBase static class and asynchronously get the connection of the service */
VisionBase.init(getContext(), new ConnectionCallback() {
@Override
public void onServiceConnect() {
/** This callback method is invoked when the service connection is successful; you can do the initialization of the detector class, mark the service connection status, and so on */
}
@Override
public void onServiceDisconnect() {
/** When the service is disconnected, this callback method is called; you can choose to reconnect the service here, or to handle the exception*/
}
});
}
Define setHiAi function to perform HiAi operation on Original image and generate the optimized Bitmap.
Code:
/**
* Capability Interfaces
*
* @return
*/
private void setHiAi() {
/** Define class detector, the context of this project is the input parameter */
ImageSuperResolution superResolution = new ImageSuperResolution(getContext());
/** Define the frame, put the bitmap that needs to detect the image into the frame*/
Frame frame = new Frame();
/** BitmapFactory.decodeFile input resource file path*/
// Bitmap bitmap = BitmapFactory.decodeFile(null);
frame.setBitmap(bitmapOriginal);
/** Define and set super-resolution parameters*/
SuperResolutionConfiguration paras = new SuperResolutionConfiguration(
SuperResolutionConfiguration.SISR_SCALE_3X,
SuperResolutionConfiguration.SISR_QUALITY_HIGH);
superResolution.setSuperResolutionConfiguration(paras);
/** Run super-resolution and get result of processing */
ImageResult result = superResolution.doSuperResolution(frame, null);
/** After the results are processed to get bitmap*/
Bitmap bmp = result.getBitmap();
/** Note: The result and the Bitmap in the result must be NULL, but also to determine whether the returned error code is 0 (0 means no error)*/
this.bitmapConverted = bmp;
handler.sendEmptyMessage(SUPERRESOLUTION_RESULT);
}
Define Handler if the optimization completed attach the optimized Bitmap to image.
Code:
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case SUPERRESOLUTION_RESULT:
if (bitmapConverted != null) {
imgConverted.setImageBitmap(bitmapConverted);
} else { // Set the original image
imgConverted.setImageBitmap(bitmapOriginal);
// toast("High Resolution image");
}
break;
}
}
};
Override the getView method attached orginial image to image view and process the original image to attached optimized image.
Code:
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
initHiAI();
int itemIndex = position;
if(convertView == null){
convertView = LayoutInflater.from(getContext()).inflate(R.layout.items,parent, false);
}
imgOriginal = convertView.findViewById(R.id.imgOriginal);
TextView imgTitle = convertView.findViewById(R.id.imgTitle);
imgConverted = convertView.findViewById(R.id.imgConverted);
bitmapOriginal = AssetsFileUtil.getBitmapByFilePath(imgOriginal.getContext(), itemList.get(itemIndex).getImgConverted());
imgOriginal.setImageBitmap(bitmapOriginal);
bitmapConverted =AssetsFileUtil.getBitmapByFilePath(imgConverted.getContext(), itemList.get(itemIndex).getImgOriginal());
imgConverted.setImageBitmap(bitmapConverted);
int height = bitmapOriginal.getHeight();
int width = bitmapOriginal.getWidth();
Log.e(TAG, "width:" + width + ";height:" + height);
if (width <= 800 && height <= 600) {
new Thread() {
@Override
public void run() {
setHiAi();
}
}.start();
} else {
toast("Width and height of the image cannot exceed 800*600");
}
imgTitle.setText(itemList.get(itemIndex).getImgTitle());
return convertView;
}
public void toast(String text) {
Toast.makeText(getContext(), text, Toast.LENGTH_SHORT).show();
}
Coding section has been complete here, now run you project and check the output of the Image Optimization by using HiAi Image Super Resolution.
5. Result
Video is visual multimedia source that combines a sequence of images to form a moving picture. The video transmits a signal to a screen and processes the order in which the screen captures should be shown.
Videos usually have audio components that correspond with the pictures being shown on the screen.
Video was first developed for “Mechanical Television” systems which were quickly replaced by cathode ray tube (CRT) and eventually replaced by flat panel displays of several types.
Huawei Video Kit brings the wonderful experience to playback the high quality videos streaming from a third party cloud platform.
Huawei Video Kit supports the streaming media in 3GP,MP4 or TS formats and comply with HTTP/HTTPS, HLS or DASH.
Huawei Video Kit will also be supporting the video hosting and editing feature in coming versions.
Features
Video Kit provides smooth playback.
It provides secure and stable solution.
It leverages the anti-leeching experience so that bandwidth is not drained out.
It allows wide ranging playback controls.
Supports playback authentication.
Development Overview
Prerequisite
Must have a Huawei Developer Account
Must have Android Studio 3.0 or later
Must have a Huawei phone with HMS Core 5.0.0.300 or later
EMUI 3.0 or later
Software Requirements
Java SDK 1.7 or later
Android 5.0 or later
Preparation
Create an app or project in the Huawei AppGallery Connect.
Provide the SHA Key and App Package name of the project in App Information Section and enable the required API.
Create an Android project.
Note: Video Kit SDK can be directly called by devices, without connecting to AppGallery Connect and hence it is not mandatory to download and integrate the agconnect-services.json.
Integration
Add below to build.gradle (project)file, under buildscript/repositories and allprojects/repositories.
Code:
<p style="line-height: normal;">Maven {url 'http://developer.huawei.com/repo/'}</p>
Add below to build.gradle (app) file, under dependencies.
Code:
<p style="line-height: normal;">implementation "com.huawei.hms:videokit-player:1.0.1.300" </p>
Adding permissions
Code:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="com.huawei.permission.SECURITY_DIAGNOSE" />
Development Process
A small trip application has been created to demonstrate the capabilities to Huawei Video Kit.
It lists all the amusement places to visit around with the use of GridView RecyclerView and card view.
To achieve it below classes are created.
Initializing WisePlayer
We have to implement a class that inherits Application and the onCreate() method has to call the initialization API WisePlayerFactory.initFactory().
Code:
public class VideoKitPlayApplication extends Application {
private static final String TAG = VideoKitPlayApplication.class.getSimpleName();
private static WisePlayerFactory wisePlayerFactory = null;
@Override
public void onCreate() {
super.onCreate();
initPlayer();
}
private void initPlayer() {
// DeviceId test is used in the demo.
WisePlayerFactoryOptions factoryOptions = new WisePlayerFactoryOptions.Builder().setDeviceId("xxx").build();
WisePlayerFactory.initFactory(this, factoryOptions, initFactoryCallback);
}
/**
* Player initialization callback
*/
private static InitFactoryCallback initFactoryCallback = new InitFactoryCallback() {
@Override
public void onSuccess(WisePlayerFactory wisePlayerFactory) {
LogUtil.i(TAG, "init player factory success");
setWisePlayerFactory(wisePlayerFactory);
}
@Override
public void onFailure(int errorCode, String reason) {
LogUtil.w(TAG, "init player factory failed :" + reason + ", errorCode is " + errorCode);
}
};
/**
* Get WisePlayer Factory
*
* @return WisePlayer Factory
*/
public static WisePlayerFactory getWisePlayerFactory() {
return wisePlayerFactory;
}
private static void setWisePlayerFactory(WisePlayerFactory wisePlayerFactory) {
VideoKitPlayApplication.wisePlayerFactory = wisePlayerFactory;
}
}
Creating instance of wise player
Code:
wisePlayer = VideoKitPlayApplication.getWisePlayerFactory().createWisePlayer();
WisePlayer layout
Code:
private void initView(View view) {
if (view != null) {
surfaceView = (SurfaceView) view.findViewById(R.id.surface_view);
textureView = (TextureView) view.findViewById(R.id.texture_view);
if (PlayControlUtil.isSurfaceView()) {
//SurfaceView display interface
SurfaceHolder surfaceHolder = surfaceView.getHolder();
surfaceHolder.addCallback(thois);
textureView.setVisibility(View.GONE);
surfaceView.setVisibility(View.VISIBLE);
} else {
//TextureView display interface
textureView.setSurfaceTextureListener(this);
textureView.setVisibility(View.VISIBLE);
surfaceView.setVisibility(View.GONE);
}
}
Register WisePlayer listeners
Code:
private void setPlayListener() {
if (wisePlayer != null) {
wisePlayer.setErrorListener(onWisePlayerListener);
wisePlayer.setEventListener(onWisePlayerListener);
wisePlayer.setResolutionUpdatedListener(onWisePlayerListener);
wisePlayer.setReadyListener(onWisePlayerListener);
wisePlayer.setLoadingListener(onWisePlayerListener);
wisePlayer.setPlayEndListener(onWisePlayerListener);
wisePlayer.setSeekEndListener(onWisePlayerListener);
}
}
Set playback parameters
Code:
player.setVideoType(PlayMode.PLAY_MODE_NORMAL);
player.setBookmark(10000);
player.setCycleMode(CycleMode.MODE_CYCLE);
Set URL for video
Code:
wisePlayer.setPlayUrl(new String[] {currentPlayData.getUrl()});
Set a view to display the video.
Code:
// SurfaceView listener callback
@Override
public void surfaceCreated(SurfaceHolder holder) {
wisePlayer.setView(surfaceView);
}
// TextureView listener callback
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
wisePlayer.setView(textureView);
// Call the resume API to bring WisePlayer to the foreground.
wisePlayer.resume(ResumeType.KEEP);
}
Prepare for the playback and start requesting data.
Code:
wisePlayer.ready();
Start the playback After success response
Code:
@Override
public void onReady(WisePlayer wisePlayer) {
player.start();
}
activity main.xml
Create a view for Recycler View.
Code:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/player_recycler_view"
android:background="@drawable/images"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="0dp" />
</RelativeLayout>
select_play_view
Create card view for displaying the trip locations.
Code:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:tag="cards main container">
<androidx.cardview.widget.CardView
android:id="@+id/card_view"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="230dp"
android:layout_margin="5dp"
card_view:cardBackgroundColor="@color/cardcolour"
card_view:cardCornerRadius="10dp"
card_view:cardElevation="5dp"
card_view:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
>
<ImageView
android:id="@+id/videoIcon"
android:tag="image_tag"
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_margin="5dp"
android:layout_weight="1"
android:src="@drawable/1"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_weight="2"
android:orientation="vertical"
>
<TextView
android:id="@+id/play_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"
android:text=""
android:textColor="@color/colorTitle"
android:textAppearance="?android:attr/textAppearanceLarge"/>
<TextView
android:id="@+id/briefStory"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_margin="10dp"
android:textColor="@color/green"
android:textAppearance="?android:attr/textAppearanceSmall"/>
<TextView
android:id="@+id/play_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textColor="@color/select_play_text_color"
android:textSize="20sp"
android:visibility="gone"/>
<TextView
android:id="@+id/play_url"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:marqueeRepeatLimit="marquee_forever"
android:maxLines="2"
android:paddingTop="5dip"
android:singleLine="false"
android:textColor="@color/select_play_text_color"
android:textSize="14sp"
android:visibility="gone"/>
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
Data Adapter to hold the data for Recycler View
Code:
package com.huawei.video.kit.demo.adapter;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.huawei.video.kit.demo.R;
import com.huawei.video.kit.demo.contract.OnItemClickListener;
import com.huawei.video.kit.demo.entity.PlayEntity;
import com.huawei.video.kit.demo.utils.LogUtil;
import com.huawei.video.kit.demo.utils.StringUtil;
import com.squareup.picasso.Picasso;
/**
* Play recyclerView adapter
*/
public class SelectPlayDataAdapter extends RecyclerView.Adapter<SelectPlayDataAdapter.PlayViewHolder> {
private static final String TAG = "SelectPlayDataAdapter";
// Data sources list
private List<PlayEntity> playList;
// Context
private Context context;
// Click item listener
private OnItemClickListener onItemClickListener;
/**
* Constructor
*
* @param context Context
* @param onItemClickListener Listener
*/
public SelectPlayDataAdapter(Context context, OnItemClickListener onItemClickListener) {
this.context = context;
playList = new ArrayList<>();
this.onItemClickListener = onItemClickListener;
}
/**
* Set list data
*
* @param playList Play data
*/
public void setSelectPlayList(List<PlayEntity> playList) {
if (this.playList.size() > 0) {
this.playList.clear();
}
this.playList.addAll(playList);
notifyDataSetChanged();
}
@NonNull
@Override
public PlayViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.select_play_item, parent, false);
return new PlayViewHolder(view);
}
@Override
public void onBindViewHolder(PlayViewHolder holder, final int position) {
if (playList.size() > position && holder != null) {
PlayEntity playEntity = playList.get(position);
if (playEntity == null) {
LogUtil.i(TAG, "current item data is empty.");
return;
}
StringUtil.setTextValue(holder.playName, playEntity.getName());
//StringUtil.setTextValue(holder.releasedYear, playEntity.getYear());
StringUtil.setTextValue(holder.briefStory, playEntity.getStory());
StringUtil.setTextValue(holder.playUrl, playEntity.getUrl());
StringUtil.setTextValue(holder.playType, String.valueOf(playEntity.getUrlType()));
holder.itemView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
onItemClickListener.onItemClick(position);
}
});
Picasso.with(context).load(playEntity.getIcon()).into(holder.videoIcon);
}
}
@Override
public int getItemCount() {
return playList.size();
}
/**
* Show view holder
*/
static class PlayViewHolder extends RecyclerView.ViewHolder {
// The video name
private TextView playName;
private TextView briefStory;
// The video type
private TextView playType;
// The video url
private TextView playUrl;
private ImageView videoIcon
/**
* Constructor
*
* @param itemView Item view
*/
public PlayViewHolder(View itemView) {
super(itemView);
if (itemView != null) {
playName = itemView.findViewById(R.id.play_name);
briefStory = itemView.findViewById(R.id.briefStory);
playType = itemView.findViewById(R.id.play_type);
playUrl = itemView.findViewById(R.id.play_url);
videoIcon = itemView.findViewById(R.id.videoIcon);
}
}
}
}
HomePageView
This associated with the view of recycler view.
Code:
package com.huawei.video.kit.demo.view;
import java.util.List;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.huawei.video.kit.demo.R;
import com.huawei.video.kit.demo.adapter.SelectPlayDataAdapter;
import com.huawei.video.kit.demo.contract.OnHomePageListener;
import com.huawei.video.kit.demo.entity.PlayEntity;
import com.huawei.video.kit.demo.utils.DialogUtil;
/**
* Home page view
*/
public class HomePageView {
int numberOfColumns = 2;
// Home page parent view
private View contentView;
// Context
private Context context;
// Play recyclerView
private RecyclerView playRecyclerView;
// Input play url
private EditText addressEt;
// Play button
private Button playBt;
// Play adapter
private SelectPlayDataAdapter selectPlayDataAdapter;
// Listener
private OnHomePageListener onHomePageListener;
/**
* Constructor
*
* @param context Context
* @param onHomePageListener Listener
*/
public HomePageView(Context context, OnHomePageListener onHomePageListener) {
this.context = context;
this.onHomePageListener = onHomePageListener;
initView();
}
/**
* Get parent view
*
* @return Parent view
*/
public View getContentView() {
return contentView;
}
/**
* Init view
*/
private void initView() {
contentView = LayoutInflater.from(context).inflate(R.layout.activity_main, null);
playRecyclerView = (RecyclerView) contentView.findViewById(R.id.player_recycler_view);
GridLayoutManager gridLayoutManager = new GridLayoutManager(context.getApplicationContext(),numberOfColumns);
playRecyclerView.setLayoutManager(gridLayoutManager);
playLoading = (ProgressBar) contentView.findViewById(R.id.play_loading);
addressEt = (EditText) contentView.findViewById(R.id.input_path_ed);
playBt = (Button) contentView.findViewById(R.id.main_play_btn);
playBt.setOnClickListener(onHomePageListener);
selectPlayDataAdapter = new SelectPlayDataAdapter(context, onHomePageListener);
// playRecyclerView.setLayoutManager(new LinearLayoutManager(context));
playRecyclerView.setAdapter(selectPlayDataAdapter);
playRecyclerView.setVisibility(View.GONE);
playLoading.setVisibility(View.VISIBLE);
}
/**
* Set the current data list
*
* @param playList Data list
*/
public void updateRecyclerView(List<PlayEntity> playList) {
selectPlayDataAdapter.setSelectPlayList(playList);
playLoading.setVisibility(View.GONE);
playRecyclerView.setVisibility(View.VISIBLE);
}
/**
* Get input text
*
* @return Text value
*/
public String getInputUrl() {
if (addressEt.getText() == null) {
return "";
} else {
return addressEt.getText().toString();
}
}
}
Results
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Conclusion
We learnt a simple application which explains the power video kit apis and showcase how easy it to have the videos running in out applications.
References
https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides-V5/introduction-0000001050439577-V5
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
In this tutorial, we will be discussing and implementing the HarmonyOS MVVM Architectural pattern in our Harmony app.
This project is available on github, link can be found at the end of the article
Table of contents
What is MVVM
Harmony MVVM example project structure
Adding dependencies
Model
Layout
Retrofit interface
ViewModel
Tip and Tricks
Conclusion
Recommended resources
What is MVVM
MVVM stands for Model, View, ViewModel:
Model: This holds the data of the application. It cannot directly talk to the View. Generally, it’s recommended to expose the data to the ViewModel through ActiveDatas (Observables ).
View: It represents the UI of the application devoid of any Application Logic. It observes the ViewModel.
ViewModel: It acts as a link between the Model and the View. It’s responsible for transforming the data from the Model. It provides data streams to the View. It also uses hooks or callbacks to update the View. It’ll ask for the data from the Model.
MVVM can be achieved in two ways:
Using Data binding
RxJava
In this tutorial we will implement MVVM in Harmony using RXjava, as Data binding is still under development and not ready to use in Harmony.
Harmony MVVM example project structure
We will create packages by features. It will make your code more modular and manageable.
Adding the Dependencies
Add the following dependencies in your module level build.gradle file:
Code:
dependencies {
//[...]
//RxJava
implementation "io.reactivex.rxjava2:rxjava:2.2.17"
//retrofit
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
implementation "com.squareup.retrofit2:converter-moshi:2.6.0"
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
//RxJava adapter for retrofit
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.7.1'
}
Model
The Model would hold the user’s email and password. The following User.java class does it:
Code:
package com.megaache.mvvmdemo.model;
public class User {
private String email;
private String password;
public User(String email, String password) {
this.email = email;
this.password = password;
}
public void setEmail(String email) {
this.email = email;
}
public String getEmail() {
return email;
}
public void setPassword(String password) {
this.password = password;
}
public String getPassword() {
return password;
}
@Override
public String toString() {
return "User{" +
"email='" + email + '\'' +
", password='" + password + '\'' +
'}';
}
}
Layout
NOTE: For this tutorial, I have decided to create the layout for smart watch devices, however it will work fine on all devices, you just need to re-arrange the components and modify the alignment.
The layout will consist of login button, two text fields and two error texts, each will be shown or hidden depending on the value of the text box above it, after clicking the login button. the final UI will like the screenshot below:
Before we create layout lets add some colors:
First create file color.json under resources/base/element and add the following json content:
Code:
{
"color": [
{
"name": "primary",
"value": "#283148"
},
{
"name": "primaryDark",
"value": "#283148"
},
{
"name": "accent",
"value": "#06EBBF"
},
{
"name": "red",
"value": "#FF406E"
}
]
}
Then, lets design background elements for the Text Fields and the Button:
Create file background_text_field.xml and background_text_button.xml under resources/base/graphic as shown in below screenshot :
Then add the following code:
Background_text_field.xml:
Code:
<?xml version="1.0" encoding="UTF-8" ?>
<shape
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:shape="rectangle">
<corners
ohos:radius="20"/>
<solid
ohos:color="#ffffff"/>
<stroke
ohos:width="2"
ohos:color="$color:accent"/>
</shape>
Background_button.xml:
Code:
<?xml version="1.0" encoding="UTF-8" ?>
<shape
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:shape="rectangle">
<corners
ohos:radius="20"/>
<solid
ohos:color="$color:accent"/>
</shape>
Now lets create the background element for the main layout, let’s called background_ability_login.xml:
Code:
<?xml version="1.0" encoding="UTF-8" ?>
<shape xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:shape="rectangle">
<solid
ohos:color="$color:primaryDark"/>
</shape>
Finally, let’s create the layout file ability_login.xml:
Code:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:id="$+id:scrollview"
ohos:height="match_parent"
ohos:width="match_parent"
ohos:background_element="$graphic:background_ability_login"
ohos:layout_alignment="horizontal_center"
ohos:rebound_effect="true"
>
<DirectionalLayout
ohos:height="match_content"
ohos:width="match_parent"
ohos:orientation="vertical"
ohos:padding="20vp"
>
<DirectionalLayout
ohos:height="match_content"
ohos:width="match_parent"
ohos:layout_alignment="center"
ohos:orientation="vertical"
>
<TextField
ohos:id="$+id:tf_email"
ohos:height="match_content"
ohos:width="match_parent"
ohos:background_element="$graphic:background_text_field"
ohos:hint="email"
ohos:left_padding="10vp"
ohos:min_height="40vp"
ohos:multiple_lines="false"
ohos:text_alignment="vertical_center"
ohos:text_color="black"
ohos:text_input_type="pattern_number"
ohos:text_size="15fp"/>
<Text
ohos:id="$+id:t_email_invalid"
ohos:height="match_content"
ohos:width="match_content"
ohos:layout_alignment="center"
ohos:text="invalid email"
ohos:text_color="$color:red"
ohos:text_size="15fp"
/>
</DirectionalLayout>
<DirectionalLayout
ohos:height="match_content"
ohos:width="match_parent"
ohos:layout_alignment="center"
ohos:orientation="vertical"
ohos:top_margin="10vp">
<TextField
ohos:id="$+id:tf_password"
ohos:height="match_content"
ohos:width="match_parent"
ohos:background_element="$graphic:background_text_field"
ohos:hint="password"
ohos:left_padding="10vp"
ohos:min_height="40vp"
ohos:multiple_lines="false"
ohos:text_alignment="vertical_center"
ohos:text_color="black"
ohos:text_input_type="pattern_password"
ohos:text_size="15fp"
/>
<Text
ohos:id="$+id:t_password_invalid"
ohos:height="match_content"
ohos:width="match_content"
ohos:layout_alignment="center"
ohos:padding="0vp"
ohos:text="invalid password"
ohos:text_color="$color:red"
ohos:text_size="15fp"
/>
</DirectionalLayout>
<Button
ohos:id="$+id:btn_login"
ohos:height="match_content"
ohos:width="match_parent"
ohos:background_element="$graphic:background_button"
ohos:bottom_margin="30vp"
ohos:min_height="40vp"
ohos:text="login"
ohos:text_color="#fff"
ohos:text_size="18fp"
ohos:top_margin="10vp"/>
</DirectionalLayout>
</ScrollView>
Retrofit interface
Before we move to the ViewModel, we have to setup our Retrofit service and repository class.
To keep the project clean, I will create class config.java which will hold our API URLs:
Code:
package com.megaache.mvvmdemo;
public class Config {
//todo: update base url variable with valid url
public static final String BASE_URL = "https://example.com";
public static final String API_VERSION = "/api/v1";
public static final String LOGIN_URL="auth/login";
}
Note: The url's are just for demonstration. For the demo to work, you must replace the urls.
First create interface APIServices.java:
For this tutorial, we assume the method of login EndPoint is Post, you may changes depending on your API, the method login will return an Observable, that will be observed in the ViewModel using RxJava.
Code:
package com.megaache.mvvmdemo.network;
import com.megaache.mvvmdemo.Config;
import com.megaache.mvvmdemo.network.request.LoginRequest;
import com.megaache.mvvmdemo.network.response.LoginResponse;
import io.reactivex.Observable;
import retrofit2.http.Body;
import retrofit2.http.Headers;
import retrofit2.http.POST;
public interface APIServices {
@POST(Config.LOGIN_URL)
@Headers("Content-Type: application/json;charset=UTF-8")
public Observable<LoginResponse> login(@Body LoginRequest loginRequest);
}
Note: the class LoginRequest which you will see later in this tutorial, must be equal to the request that the server expects in The names of variables and their types, otherwise the server will fail to process the request.
Then, add method createRetrofitClient() to MyApplication.java, which will create and return retrofit instance, the instance will use Moshi converter to handle the conversion of JSON to our java class, and RxJava2 adapter to return observables that can work with RxJava instead of the default Call class which requires callbacks:
Code:
package com.megaache.mvvmdemo;
import com.megaache.mvvmdemo.network.APIServices;
import ohos.aafwk.ability.AbilityPackage;
import okhttp3.OkHttpClient;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import retrofit2.converter.moshi.MoshiConverterFactory;
import java.util.concurrent.TimeUnit;
public class MyApplication extends AbilityPackage {
@Override
public void onInitialize() {
super.onInitialize();
}
public static APIServices createRetrofitClient() {
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(60L, TimeUnit.SECONDS)
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(Config.BASE_URL + Config.API_VERSION)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(MoshiConverterFactory.create()).client(client)
.build();
return retrofit.create(APIServices.class);
}
}
NOTE: For cleaner code, you can create file RetrofitClient.java and move the method createRetrofitClient() to it.
Now, let’s work on the Login feature, we going to first create request and response classes, then move to the ViewModel and the view:
We need LoginRequest and LoginResponse which both will extends BaseRequest and BaseRespnonse, code is shown below:
Create BaseRequest.java:
In real life project, your API may expect some parameter to be sent with every request. For example: accessToken, language, deviceId, pushToken etc. which will depende on your API. for this tutorial I added one field called deviceType with static value.
Code:
package com.megaache.mvvmdemo.network.request;
public class BaseRequest {
private String deviceType;
public BaseRequest() {
deviceType = "harmony-watch";
}
public String getDeviceType() {
return deviceType;
}
public void setDeviceType(String deviceType) {
this.deviceType = deviceType;
}
}
Create class LoginRequest.java, which will extend BaseRequest and have two fields Email and Password, which will provided by the end user:
Code:
package com.megaache.mvvmdemo.network.request;
public class LoginRequest {
private String email;
private String password;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
Then for the response, Create BaseResponse.java first:
Code:
package com.megaache.mvvmdemo.network.response;
import com.megaache.mvvmdemo.MyApplication;
import java.io.Serializable;
public class BaseResponse implements Serializable {
}
Then LoginResponse.java extending BaseResponse:
Code:
package com.megaache.mvvmdemo.network.response;
import com.megaache.mvvmdemo.model.User;
import com.squareup.moshi.Json;
public class LoginResponse extends BaseResponse {
@Json(name = "user")
private User user;
@Json(name = "accessToken")
private String accessToken;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
}
Note: this class must be equal to the response you get from server, otherwise Retrofit Gson converter will fail to convert the response to LoginResponse class, both the type of variables and their names must equal the those in the JSON response.
ViewModel
In ViewModel, we will wrap the data which was loaded with Retrofit inside class LoggedIn in LoginViewState, and observe states Observable defined in BaseViewModel in our Ability (or AbilitySlice). Whenever the value in states changes, the ability will be notified without checking whether the ability is alive or not.
The code for LoginViewState.java extending empty class BaseViewState.java, and ErrorData.java (used in LoginViewState.java) is given below:
ErrorData.java:
Code:
package com.megaache.mvvmdemo.model;
import java.io.Serializable;
public class ErrorData implements Serializable {
private String message;
private int statusCode;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public int getStatusCode() {
return statusCode;
}
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
}
LoginViewState.java:
Code:
package com.megaache.mvvmdemo.ui.login;
import com.megaache.mvvmdemo.base.BaseViewState;
import com.megaache.mvvmdemo.model.ErrorData;
import com.megaache.mvvmdemo.network.response.LoginResponse;
public class LoginViewState extends BaseViewState {
public static class Loading extends LoginViewState {
}
public static class Error extends LoginViewState {
private ErrorData message;
public Error(ErrorData message) {
this.message = message;
}
public void setMessage(ErrorData message) {
this.message = message;
}
public ErrorData getMessage() {
return message;
}
}
public static class LoggedIn extends LoginViewState {
private LoginResponse userDataResponse;
public LoggedIn(LoginResponse userDataResponse) {
this.userDataResponse = userDataResponse;
}
public LoginResponse getUserDataResponse() {
return userDataResponse;
}
public void setUserDataResponse(LoginResponse userDataResponse) {
this.userDataResponse = userDataResponse;
}
}
}
The code for the LoginViewModel.java is given below:
When the user clicks the login button, the method sendLoginRequest() will setup our retrofit Observable, the request will not be sent until the we call the method subscribe which will be done on the View. notice we are subscribing on the Schedulers.Io() Scheduler, which is will execute the requests in a background thread to avoid freezing the UI, and because of that we have to create our custom Observer that will invoke the callback code in UI thread after we receive data, more on this later:
Code:
package com.megaache.mvvmdemo.ui.login;
import com.megaache.mvvmdemo.base.BaseViewModel;
import com.megaache.mvvmdemo.MyApplication;
import com.megaache.mvvmdemo.model.ErrorData;
import com.megaache.mvvmdemo.network.request.LoginRequest;
import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
import ohos.aafwk.abilityjet.activedata.ActiveData;
public class LoginViewModel extends BaseViewModel<LoginViewState> {
private static final int MIN_PASSWORD_LENGTH = 6;
public ActiveData<Boolean> emailValid = new ActiveData<>();
public ActiveData<Boolean> passwordValid = new ActiveData<>();
public ActiveData<Boolean> loginState = new ActiveData<>();
public LoginViewModel() {
super();
}
public void login(String email, String password) {
boolean isEmailValid = isEmailValid(email);
emailValid.setData(isEmailValid);
if (!isEmailValid)
return;
boolean isPasswordValid = isPasswordValid(email);
passwordValid.setData(isPasswordValid);
if (!isPasswordValid)
return;
LoginRequest loginRequest = new LoginRequest();
loginRequest.setEmail(email);
loginRequest.setPassword(password);
super.subscribe(sendLoginRequest(loginRequest));
}
private Observable<LoginViewState> sendLoginRequest(LoginRequest loginRequest) {
return MyApplication.createRetrofitClient()
.login(loginRequest)
.doOnError(Throwable::printStackTrace)
.map(LoginViewState.LoggedIn::new)
.cast(LoginViewState.class)
.onErrorReturn(throwable -> {
ErrorData errorData = new ErrorData();
if (throwable.getMessage() != null)
errorData.setMessage(throwable.getMessage());
else
errorData.setMessage(" No internet! ");
return new LoginViewState.Error(errorData);
})
.subscribeOn(Schedulers.io())
.startWith(new LoginViewState.Loading());
}
private boolean isEmailValid(String email) {
return email != null && !email.isEmpty() && email.contains("@");
}
private boolean isPasswordValid(String password) {
return password != null && password.length() > MIN_PASSWORD_LENGTH;
}
}
Settings up the ability (View)
As you know, ability is our view, we have instantiated ViewModel and observer states and ActiveDatas in the method ObserverData(), as mentioned before, retrofit will send the request on background thread, therefore the code in the Observer will run on the same thread (Schedulars.io()), which will cause exceptions if that code attemp to update the UI, to prevent that, we will create a custom UIObserver class which extends Observer, that will run our code in the UI task dispatcher of the ability (UI Thread), code for UiObserver.java as show below:
Code:
package com.megaache.mvvmdemo.utils;
import ohos.aafwk.ability.Ability;
import ohos.aafwk.ability.AbilitySlice;
import ohos.aafwk.abilityjet.activedata.DataObserver;
import ohos.app.dispatcher.TaskDispatcher;
public abstract class UiObserver<T> extends DataObserver<T> {
private TaskDispatcher uiTaskDispatcher;
public UiObserver(Ability baseAbilitySlice) {
setLifecycle(baseAbilitySlice.getLifecycle());
uiTaskDispatcher = baseAbilitySlice.getUITaskDispatcher();
}
@Override
public void onChanged(T t) {
uiTaskDispatcher.asyncDispatch(() -> onValueChanged(t));
}
public abstract void onValueChanged(T t);
}
Code for LoginAbility.java is shown below:
Code:
package com.megaache.mvvmdemo.ui.login;
import com.megaache.mvvmdemo.ResourceTable;
import com.megaache.mvvmdemo.utils.UiObserver;
import com.megaache.mvvmdemo.model.ErrorData;
import ohos.aafwk.ability.Ability;
import ohos.aafwk.content.Intent;
import ohos.agp.components.Button;
import ohos.agp.components.Component;
import ohos.agp.components.Text;
import ohos.agp.components.TextField;
import ohos.agp.window.dialog.ToastDialog;
public class LoginAbility extends Ability {
private LoginViewModel loginViewModel;
private TextField emailTF;
private Text emailInvalidT;
private TextField passwordTF;
private Text passwordInvalidT;
@Override
public void onStart(Intent intent) {
super.onStart(intent);
loginViewModel = new LoginViewModel();
initUI();
observeData();
}
private void initUI() {
super.setUIContent(ResourceTable.Layout_ability_login);
Button loginButton = (Button) findComponentById(ResourceTable.Id_btn_login);
loginButton.setClickedListener(c -> attemptLogin());
emailTF = (TextField) findComponentById(ResourceTable.Id_tf_email);
emailInvalidT = (Text) findComponentById(ResourceTable.Id_t_email_invalid);
passwordTF = (TextField) findComponentById(ResourceTable.Id_tf_password);
passwordInvalidT = (Text) findComponentById(ResourceTable.Id_t_password_invalid);
}
private void observeData() {
loginViewModel.emailValid.addObserver(new UiObserver<Boolean>(this) {
@Override
public void onValueChanged(Boolean aBoolean) {
emailInvalidT.setVisibility(aBoolean ? Component.VISIBLE : Component.HIDE);
}
}, false);
loginViewModel.passwordValid.addObserver(new UiObserver<Boolean>(this) {
@Override
public void onValueChanged(Boolean aBoolean) {
passwordInvalidT.setVisibility(aBoolean ? Component.VISIBLE : Component.HIDE);
}
}, false);
loginViewModel.getStates().addObserver(new UiObserver<LoginViewState>(this) {
@Override
public void onValueChanged(LoginViewState loginState) {
if (loginState instanceof LoginViewState.Loading) {
toggleLoadingDialog(true);
} else if (loginState instanceof LoginViewState.Error) {
toggleLoadingDialog(false);
manageError(((LoginViewState.Error) loginState).getMessage());
} else if (loginState instanceof LoginViewState.LoggedIn) {
toggleLoadingDialog(false);
showToast("logging successful!");
}
}
}, false);
}
private void attemptLogin() {
loginViewModel.login(emailTF.getText(), passwordTF.getText());
}
private void toggleLoadingDialog(boolean show) {
//todo: show/hide loading dialog
}
private void manageError(ErrorData errorData) {
showToast(errorData.getMessage());
}
private void showToast(String message) {
new ToastDialog(this)
.setText(message)
.show();
}
@Override
protected void onStop() {
super.onStop();
loginViewModel.unbind();
}
}
Tips And Tricks
If you want your app to work offline, its best to introduce a Repository classes that will handle quering information from server if internet is available or from the cach if not
For cleaner code, try re-using the ViewModel as much as possible, by creating a base class and moving the shared code their
You should not keep a reference to a View (component) or context in the ViewModel, unless you have no option
The ViewModel should not talk directly to the View, instead the View obseve the ViewModel and update itself depending on ViewModel data
A correct implementation of ViewModel should allow you to change the UI with minimal or zero changes to the ViewModel.
Conclusion
MVVM combines the advantages of separation of concerns provided by MVP archichetecture, while leveraging the advantages of RxJava or Data binding. The result is a pattern where the model drives as many of the operations as possible, minimizing the logic in the view.
Finally, talk is cheap, and I strongly advise you to try and learn these things in the code so that you do not need to rely on people like me to tell you what to do.
Clone the project from github, replate the API Urls in class config.java and run it on HarmonyOs Device, you should see a toast that says "Logging succesful" if the credentials are correct, otherwise it should show a toast with the error that says "no internet" or and error returned from the server.
This project is available on GitHub: Click here
Recommended sources:
HarmonyOS (essential topics): Essential Topics
Retrofit: Click here
Rxjava: Click here
Original Source
Comment below if you have any questions or suggestions.
Thank you!
In Android we have Manifest file right there we will declare all the activity names like that do we have any file here?
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Introduction
HarmonyOs is a next-generation operating system that empowers interconnection and collaboration between smart devices. It delivers smooth simple interaction that is reliable in all scenarios.
SQLite is an open-source relational database which is used to perform database operations on devices such as storing, manipulating or retrieving persistent data from the database.
HarmonyOs uses SQLite DB for managing local database and called it is as HarmonyOs RDB (relational database).
Takeaways
Integrate HarmonyOs RDB in the application.
Navigate from one Ability Slice to another and sending data while doing it.
Learn to create UI using Directional Layout.
Default and customize Dialog.
Providing background color to buttons or layout programmatically.
HarmonyOs Animation.
Demo
To understand how HarmonyOs works with SQLite DB, I have created a Quiz App and inserted all the questions data using SQLite database as shown below:
Integrating HarmonyOs RDB
Step 1: Create Questions model (POJO) class.
Java:
public class Questions {
private int id;
private String topic;
private String question;
private String optionA;
private String optionB;
private String optionC;
private String optionD;
private String answer;
public Questions(String topc, String ques, String opta, String optb, String optc, String optd, String ans) {
topic = topc;
question = ques;
optionA = opta;
optionB = optb;
optionC = optc;
optionD = optd;
answer = ans;
}
public Questions() {
id = 0;
topic = "";
question = "";
optionA = "";
optionB = "";
optionC = "";
optionD = "";
answer = "";
}
public void setId(int id) {
this.id = id;
}
public String getTopic() {
return topic;
}
public void setTopic(String topic) {
this.topic = topic;
}
public String getQuestion() {
return question;
}
public void setQuestion(String question) {
this.question = question;
}
public String getOptionA() {
return optionA;
}
public void setOptionA(String optionA) {
this.optionA = optionA;
}
public String getOptionB() {
return optionB;
}
public void setOptionB(String optionB) {
this.optionB = optionB;
}
public String getOptionC() {
return optionC;
}
public void setOptionC(String optionC) {
this.optionC = optionC;
}
public String getOptionD() {
return optionD;
}
public void setOptionD(String optionD) {
this.optionD = optionD;
}
public String getAnswer() {
return answer;
}
public void setAnswer(String answer) {
this.answer = answer;
}
}
Step 2: Create a class and name it as QuizDatabaseHelper.
Step 3: Extends the class with DatabaseHelper class.
Step 4: After that we need to configure the RDB store. For that we need to use StoreConfig.
Java:
StoreConfig config = StoreConfig.newDefaultConfig("QuizMania.db");
Step 5: Use RdbOpenCallback abstract class to create the table and if we need to modify the table, we can use this class to upgrade the version of the Database to avoid crashes.
Java:
RdbOpenCallback callback = new RdbOpenCallback() {
@Override
public void onCreate(RdbStore store) {
store.executeSql("CREATE TABLE " + TABLE_NAME + " ( " + ID + " INTEGER PRIMARY KEY AUTOINCREMENT , " + TOPIC + " VARCHAR(255), " + QUESTION + " VARCHAR(255), " + OPTIONA + " VARCHAR(255), " + OPTIONB + " VARCHAR(255), " + OPTIONC + " VARCHAR(255), " + OPTIOND + " VARCHAR(255), " + ANSWER + " VARCHAR(255))");
}
@Override
public void onUpgrade(RdbStore store, int oldVersion, int newVersion) {
}
};
Step 6: Use DatabaseHelper class to obtain the RDB store.
Java:
DatabaseHelper helper = new DatabaseHelper(context);
store = helper.getRdbStore(config, 1, callback, null);
Step 7: In order to insert question data we will use ValueBucket of RDB.
Java:
private void insertAllQuestions(ArrayList<Questions> allQuestions){
ValuesBucket values = new ValuesBucket();
for(Questions question : allQuestions){
values.putString(TOPIC, question.getTopic());
values.putString(QUESTION, question.getQuestion());
values.putString(OPTIONA, question.getOptionA());
values.putString(OPTIONB, question.getOptionB());
values.putString(OPTIONC, question.getOptionC());
values.putString(OPTIOND, question.getOptionD());
values.putString(ANSWER, question.getAnswer());
long id = store.insert("QUIZMASTER", values);
}
}
Step 8: In order to retrieve all the question data we will use RdbPredicates and ResultSet. RdbPredicates helps us to combine SQL statements simply by calling methods using this class, such as equalTo, notEqualTo, groupBy, orderByAsc, and beginsWith. ResultSet on the other hand helps us to retrieve the data that we have queried.
Java:
public List<Questions> getAllListOfQuestions(String topicName) {
List<Questions> questionsList = new ArrayList<>();
String[] columns = new String[] {ID, TOPIC, QUESTION, OPTIONA,OPTIONB,OPTIONC,OPTIOND,ANSWER};
RdbPredicates rdbPredicates = new RdbPredicates(TABLE_NAME).equalTo(TOPIC, topicName);
ResultSet resultSet = store.query(rdbPredicates, columns);
while (resultSet.goToNextRow()){
Questions question = new Questions();
question.setId(resultSet.getInt(0));
question.setTopic(resultSet.getString(1));
question.setQuestion(resultSet.getString(2));
question.setOptionA(resultSet.getString(3));
question.setOptionB(resultSet.getString(4));
question.setOptionC(resultSet.getString(5));
question.setOptionD(resultSet.getString(6));
question.setAnswer(resultSet.getString(7));
questionsList.add(question);
}
return questionsList;
}
Step 9: Let's call the QuizDatabaseHelper class in Ability Slice and get all the question from the stored database.
Java:
QuizDatabaseHelper quizDatabaseHelper = new QuizDatabaseHelper(getContext());
quizDatabaseHelper.initDb();
if (quizDatabaseHelper.getAllListOfQuestions(topicName).size() == 0) {
quizDatabaseHelper.listOfAllQuestion();
}
List<Questions> list = quizDatabaseHelper.getAllListOfQuestions(topicName);
Collections.shuffle(list);
Questions questionObj = list.get(questionId);
QuizDatabaseHelper.java
Java:
public class QuizDatabaseHelper extends DatabaseHelper {
Context context;
StoreConfig config;
RdbStore store;
private static final String TABLE_NAME = "QUIZMASTER";
private static final String ID = "_ID";
private static final String TOPIC = "TOPIC";
private static final String QUESTION = "QUESTION";
private static final String OPTIONA = "OPTIONA";
private static final String OPTIONB = "OPTIONB";
private static final String OPTIONC = "OPTIONC";
private static final String OPTIOND = "OPTIOND";
private static final String ANSWER = "ANSWER";
public QuizDatabaseHelper(Context context) {
super(context);
this.context = context;
}
public void initDb(){
config = StoreConfig.newDefaultConfig("QuizMania.db");
RdbOpenCallback callback = new RdbOpenCallback() {
@Override
public void onCreate(RdbStore store) {
store.executeSql("CREATE TABLE " + TABLE_NAME + " ( " + ID + " INTEGER PRIMARY KEY AUTOINCREMENT , " + TOPIC + " VARCHAR(255), " + QUESTION + " VARCHAR(255), " + OPTIONA + " VARCHAR(255), " + OPTIONB + " VARCHAR(255), " + OPTIONC + " VARCHAR(255), " + OPTIOND + " VARCHAR(255), " + ANSWER + " VARCHAR(255))");
}
@Override
public void onUpgrade(RdbStore store, int oldVersion, int newVersion) {
}
};
DatabaseHelper helper = new DatabaseHelper(context);
store = helper.getRdbStore(config, 1, callback, null);
}
public void listOfAllQuestion() {
// Generic type is Questions POJO class.
ArrayList<Questions> arraylist = new ArrayList<>();
// General Knowledge Questions...
arraylist.add(new Questions("gk","India has largest deposits of ____ in the world.", "Gold", "Copper", "Mica", "None of the above", "Mica"));
arraylist.add(new Questions("gk","Who was known as Iron man of India ?", "Govind Ballabh Pant", "Jawaharlal Nehru", "Subhash Chandra Bose", "Sardar Vallabhbhai Patel", "Sardar Vallabhbhai Patel"));
arraylist.add(new Questions("gk", "India participated in Olympics Hockey in", "1918", "1928", "1938", "1948", "1928"));
arraylist.add(new Questions("gk","Who is the Flying Sikh of India ?", "Mohinder Singh", "Joginder Singh", "Ajit Pal Singh", "Milkha singh", "Milkha singh"));
arraylist.add(new Questions("gk","How many times has Brazil won the World Cup Football Championship ?", "Four times", "Twice", "Five times", "Once", "Five times"));
// Sports Questions..
arraylist.add(new Questions("sp","Which was the 1st non Test playing country to beat India in an international match ?", "Canada", "Sri Lanka", "Zimbabwe", "East Africa", "Sri Lanka"));
arraylist.add(new Questions("sp","Ricky Ponting is also known as what ?", "The Rickster", "Ponts", "Ponter", "Punter", "Punter"));
arraylist.add(new Questions("sp","India won its first Olympic hockey gold in...?", "1928", "1932", "1936", "1948", "1928"));
arraylist.add(new Questions("sp","The Asian Games were held in Delhi for the first time in...?", "1951", "1963", "1971", "1982", "1951"));
arraylist.add(new Questions("sp","The 'Dronacharya Award' is given to...?", "Sportsmen", "Coaches", "Umpires", "Sports Editors", "Coaches"));
// History Questions...
arraylist.add(new Questions("his","The Battle of Plassey was fought in", "1757", "1782", "1748", "1764", "1757"));
arraylist.add(new Questions("his","The title of 'Viceroy' was added to the office of the Governor-General of India for the first time in", "1848 AD", "1856 AD", "1858 AD", "1862 AD", "1858 AD"));
arraylist.add(new Questions("his","Tipu sultan was the ruler of", "Hyderabad", "Madurai", "Mysore", "Vijayanagar", "Mysore"));
arraylist.add(new Questions("his","The Vedas contain all the truth was interpreted by", "Swami Vivekananda", "Swami Dayananda", "Raja Rammohan Roy", "None of the above", "Swami Dayananda"));
arraylist.add(new Questions("his","The Upanishads are", "A source of Hindu philosophy", "Books of ancient Hindu laws", "Books on social behavior of man", "Prayers to God", "A source of Hindu philosophy"));
// General Science Questions...
arraylist.add(new Questions("gs","Which of the following is a non metal that remains liquid at room temperature ?", "Phosphorous", "Bromine", "Chlorine", "Helium", "Bromine"));
arraylist.add(new Questions("gs","Which of the following is used in pencils?", "Graphite", "Silicon", "Charcoal", "Phosphorous", "Graphite"));
arraylist.add(new Questions("gs","The gas usually filled in the electric bulb is", "Nitrogen", "Hydrogen", "Carbon Dioxide", "Oxygen", "Nitrogen"));
arraylist.add(new Questions("gs","Which of the gas is not known as green house gas ?", "Methane", "Nitrous oxide", "Carbon dioxide", "Hydrogen", "Hydrogen"));
arraylist.add(new Questions("gs","The hardest substance available on earth is", "Gold", "Iron", "Diamond", "Platinum", "Diamond"));
this.insertAllQuestions(arraylist);
}
private void insertAllQuestions(ArrayList<Questions> allQuestions){
ValuesBucket values = new ValuesBucket();
for(Questions question : allQuestions){
values.putString(TOPIC, question.getTopic());
values.putString(QUESTION, question.getQuestion());
values.putString(OPTIONA, question.getOptionA());
values.putString(OPTIONB, question.getOptionB());
values.putString(OPTIONC, question.getOptionC());
values.putString(OPTIOND, question.getOptionD());
values.putString(ANSWER, question.getAnswer());
long id = store.insert("QUIZMASTER", values);
}
}
public List<Questions> getAllListOfQuestions(String topicName) {
List<Questions> questionsList = new ArrayList<>();
String[] columns = new String[] {ID, TOPIC, QUESTION, OPTIONA,OPTIONB,OPTIONC,OPTIOND,ANSWER};
RdbPredicates rdbPredicates = new RdbPredicates(TABLE_NAME).equalTo(TOPIC, topicName);
ResultSet resultSet = store.query(rdbPredicates, columns);
while (resultSet.goToNextRow()){
Questions question = new Questions();
question.setId(resultSet.getInt(0));
question.setTopic(resultSet.getString(1));
question.setQuestion(resultSet.getString(2));
question.setOptionA(resultSet.getString(3));
question.setOptionB(resultSet.getString(4));
question.setOptionC(resultSet.getString(5));
question.setOptionD(resultSet.getString(6));
question.setAnswer(resultSet.getString(7));
questionsList.add(question);
}
return questionsList;
}
}
HarmonyOs Navigation
An Ability Slice represents a single screen and its control logic. In terms of Android, it is like a Fragment and Page Ability is like an Activity in Android. An ability slice's lifecycle is bound to the Page ability that hosts it.
Now, if we need to navigate with data from one Ability Slice to another, we need to use present method of HarmonyOs.
Java:
public final void present(AbilitySlice targetSlice, Intent intent) {
throw new RuntimeException("Stub!");
}
GameAbilitySlice.java
Java:
private void goToQuizPage(String topic){
Intent intent = new Intent();
intent.setParam("TEST_KEY", topic);
present(new QuizAbilitySlice(), intent);
}
Here the targetSlice is QuizAbilitySlice.
QuizAbilitySlice.java
Java:
String topicName = intent.getStringParam("TEST_KEY");
Here we getting the value from the source Ability Slice.
HarmonyOs User Interface
Layouts
There six layouts available in HarmonyOs:
DirectionalLayout
DependentLayout
StackLayout
TableLayout
PositionLayout
AdaptiveBoxLayout
We will be using DirectionalLayout for our UI. In terms of Android, it is like LinearLayout. It has orientation, weight and many more which we will find in LinearLayout as well.
Text and Button Components
Yes you heard it right. Any widget in HarmonyOs is treated as Component. Here Text as well Button are Component of HarmonyOs. As HarmonyOs uses XML for UI, all those XML properties which we see in Android can be use here. The only difference which we will find here is providing the background colour to Buttons or Layout. In order to provide background colour, we need to create a graphic XML file under the graphic folder of resource.
btn_option.xml
XML:
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:shape="rectangle">
<corners
ohos:radius="20"/>
<solid
ohos:color="#2c3e50"/>
</shape>
After that we will use button_option.xml file as background colour for buttons using background_element property.
Java:
<Button
ohos:id="$+id:btnD"
ohos:height="80fp"
ohos:width="match_parent"
ohos:margin="10fp"
ohos:text_color="#ecf0f1"
ohos:text_size="30fp"
ohos:text="Gold"
ohos:background_element="$graphic:btn_option"/>
ability_quiz.xml
XML:
<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:height="match_parent"
ohos:width="match_parent"
ohos:alignment="center"
ohos:orientation="vertical">
<DirectionalLayout
ohos:height="match_parent"
ohos:width="match_parent"
ohos:orientation="vertical"
ohos:weight="0.5"
ohos:alignment="center"
ohos:background_element="$graphic:background_question_area">
<Text
ohos:id="$+id:txtQuestion"
ohos:height="match_content"
ohos:width="match_content"
ohos:text_alignment="center"
ohos:multiple_lines="true"
ohos:margin="20fp"
ohos:text_size="40vp"
ohos:text="Question"
/>
</DirectionalLayout>
<DirectionalLayout
ohos:height="match_parent"
ohos:width="match_parent"
ohos:orientation="vertical"
ohos:alignment="center"
ohos:weight="1">
<Button
ohos:id="$+id:btnA"
ohos:height="80fp"
ohos:width="match_parent"
ohos:margin="10fp"
ohos:text_color="#ecf0f1"
ohos:text_size="30fp"
ohos:text="Gold"
ohos:background_element="$graphic:btn_option"
/>
<Button
ohos:id="$+id:btnB"
ohos:height="80fp"
ohos:width="match_parent"
ohos:margin="10fp"
ohos:text_color="#ecf0f1"
ohos:text_size="30fp"
ohos:text="Gold"
ohos:background_element="$graphic:btn_option"
/>
<Button
ohos:id="$+id:btnC"
ohos:height="80fp"
ohos:width="match_parent"
ohos:margin="10fp"
ohos:text_color="#ecf0f1"
ohos:text_size="30fp"
ohos:text="Gold"
ohos:background_element="$graphic:btn_option"
/>
<Button
ohos:id="$+id:btnD"
ohos:height="80fp"
ohos:width="match_parent"
ohos:margin="10fp"
ohos:text_color="#ecf0f1"
ohos:text_size="30fp"
ohos:text="Gold"
ohos:background_element="$graphic:btn_option"
/>
</DirectionalLayout>
</DirectionalLayout>
HarmonyOs Dialogs
There are five Dialog available in HarmonyOs to use:
DisplayDialog
CommonDialog
BaseDialog
PopupDialog
ListDialog
ToastDialog
We will be using CommonDialog to show default as well as customize dialog in our application. Dialog in HarmonyOs is also known as Component. CommonDialog helps us to provide Button like functionality as we see in Android Dialogs.
Default CommonDialog
Java:
private void wrongAnsDialog(){
CommonDialog commonDialog = new CommonDialog(getContext());
commonDialog.setTitleText("WRONG ANSWER");
commonDialog.setSize(1000,300);
commonDialog.setButton(1, "OKAY", new IDialog.ClickedListener() {
@Override
public void onClick(IDialog iDialog, int i) {
commonDialog.hide();
present(new GameAbilitySlice(), new Intent());
}
});
commonDialog.show();
}
Customize CommonDialog
Java:
private void correctAnsDialog(){
CommonDialog commonDialog = new CommonDialog(getContext());
DependentLayout dependentLayout = new DependentLayout (getContext());
dependentLayout.setWidth(DependentLayout.LayoutConfig.MATCH_PARENT);
dependentLayout.setHeight(DependentLayout.LayoutConfig.MATCH_PARENT);
dependentLayout.setBackground(new ShapeElement(this,ResourceTable.Graphic_correct_dialog));
Text text = new Text(getContext());
text.setText("CORRECT ANSWER");
text.setTextSize(60);
text.setTextColor(Color.WHITE);
DependentLayout.LayoutConfig textConfig = new DependentLayout.LayoutConfig(DependentLayout.LayoutConfig.MATCH_CONTENT,
DependentLayout.LayoutConfig.MATCH_CONTENT);
textConfig.addRule(DependentLayout.LayoutConfig.CENTER_IN_PARENT);
textConfig.addRule(DependentLayout.LayoutConfig.ALIGN_PARENT_TOP);
text.setLayoutConfig(textConfig);
Button btnNext = new Button(getContext());
btnNext.setText("NEXT QUESTION");
btnNext.setClickedListener(new Component.ClickedListener() {
@Override
public void onClick(Component component) {
commonDialog.hide();
questionId++;
questionObj = list.get(questionId);
onNextQuestionAndOption();
resetButtonColors();
enableAllButtons();
}
});
btnNext.setBackground(new ShapeElement(this,ResourceTable.Graphic_btn_next));
btnNext.setTextColor(Color.BLACK);
btnNext.setPadding(20,20,20,20);
btnNext.setTextSize(50);
DependentLayout.LayoutConfig btnConfig = new DependentLayout.LayoutConfig(DependentLayout.LayoutConfig.MATCH_PARENT,
DependentLayout.LayoutConfig.MATCH_CONTENT);
btnConfig.addRule(DependentLayout.LayoutConfig.CENTER_IN_PARENT);
btnConfig.addRule(DependentLayout.LayoutConfig.ALIGN_PARENT_BOTTOM);
btnNext.setLayoutConfig(btnConfig);
dependentLayout.addComponent(text);
dependentLayout.addComponent(btnNext);
commonDialog.setContentCustomComponent(dependentLayout);
commonDialog.setSize(1000,300);
commonDialog.show();
}
Programmatically changing color
In order to change color programmatically to buttons or layout we use ShapeElement class.
Java:
// For Buttons …
private void resetButtonColors() {
btnA.setBackground(new ShapeElement(this,ResourceTable.Graphic_btn_option));
btnB.setBackground(new ShapeElement(this,ResourceTable.Graphic_btn_option));
btnC.setBackground(new ShapeElement(this,ResourceTable.Graphic_btn_option));
btnD.setBackground(new ShapeElement(this,ResourceTable.Graphic_btn_option));
}
// For Layouts …
DependentLayout dependentLayout = new DependentLayout (getContext());
dependentLayout.setWidth(DependentLayout.LayoutConfig.MATCH_PARENT);
dependentLayout.setHeight(DependentLayout.LayoutConfig.MATCH_PARENT);
dependentLayout.setBackground(new ShapeElement(this,ResourceTable.Graphic_correct_dialog));
Here ResourceTable is treated same as R in Android.
HarmonyOs Animation
HarmonyOs provides three major classes for animation:
FrameAnimationElement
AnimatorValue
AnimatorProperty
AnimatorGroup.
We will be using AnimatorProperty to do our animation in our splash screen.
Step 1: We need to create AnimatorProperty Object.
Java:
AnimatorProperty topAnim = logImg.createAnimatorProperty();
topAnim.alphaFrom((float) 0.1).alpha((float) 1.0).moveFromY(0).moveToY(700).setDuration(2000);
Here logImg is an Image.
Step 2: Create animator_property.xml file in resource/base/animation folder.
Code:
<?xml version="1.0" encoding="UTF-8" ?>
<animator xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:duration="2000"/>
Step 3: Parse the animator_property.xml file and use its configuration using AnimatorScatter class.
Java:
AnimatorScatter scatter = AnimatorScatter.getInstance(getContext());
Animator animator = scatter.parse(ResourceTable.Animation_topanim);
if (animator instanceof AnimatorProperty) {
topAnim = (AnimatorProperty) animator;
topAnim.setTarget(logImg);
topAnim.moveFromY(0).moveToY(700);
}
logImg.setBindStateChangedListener(new Component.BindStateChangedListener() {
@Override
public void onComponentBoundToWindow(Component component) {
topAnim.start();
}
@Override
public void onComponentUnboundFromWindow(Component component) {
topAnim.stop();
}
});
Step 4: Start Animation
Java:
topAnim.start();
Tips & Tricks
Kindly follow my article, my entire article is full of tips & tricks. I have also mentioned Android keywords to make android developers familiar with the terminology of HarmonyOs.
Conclusion
In this article, we learn how to integrate SQLite DB in HarmonyOs application. Now you can use this knowledge and create application such as Library Management, School Management, Games etc.
Feel free to comment, share and like the article. Also you can follow me to get awesome article like this every week.
For more reference
https://developer.harmonyos.com/en/docs/documentation/doc-guides/database-relational-overview-0000000000030046
https://developer.harmonyos.com/en/docs/documentation/doc-guides/ui-java-overview-0000000000500404
Original Source
IntroductionIf are you new to this application, please follow my previous articles
Pygmy collection application Part 1 (Account kit)
Intermediate: Pygmy Collection Application Part 2 (Ads Kit)
Intermediate: Pygmy Collection Application Part 3 (Crash service)
Intermediate: Pygmy Collection Application Part 4 (Analytics Kit Custom Events)
Intermediate: Pygmy Collection Application Part 5 (Safety Detect)
Intermediate: Pygmy Collection Application Part 6 (Room database)
Intermediate: Pygmy Collection Application Part 7 (Document Skew correction Huawei HiAI)
Click to expand...
Click to collapse
In this article, we will learn how to integrate Huawei Scan Kit in Pygmy collection finance application.
HUAWEI Scan Kit scans and parses all major 1D and 2D barcodes as well as generates barcodes to help you to quickly build barcode scanning functions into your apps. Scan Kit automatically detects, magnifies, and recognizes barcodes from a distance, and also can scan a very small barcode in the same way. It works even in suboptimal situations, such as under dim lighting or when the barcode is reflective, dirty, blurry, or printed on a cylindrical surface. This leads to a higher scanning success rate, and an improved user experience.
Scan Kit Capabilities:
13 global barcode format supported
Long range of detection
Auto Zoom
Orientation Independent
Multi-code recognition
Runs on device
Doesn’t need Internet connection
Best latency and accuracy provided
Recognition in complex scenarios as well.
There are three type of scan type.
Default View
Customized View
Multiprocessor Camera
Default View: In Default View mode, Scan Kit scans the barcodes using the camera or from images in the album. You do not need to worry about designing a UI as Scan Kit provides one.
Customized View: In Customized View mode, you do not need to worry about developing the scanning process or camera control. Scan Kit will do all these tasks for you. However, you will need to customize the scanning UI according to the customization options that Flutter Scan Plugin provides. This can also be easily completed based on the sample code below.
Multiprocessor Camera: Multiprocessor Camera Mode is used to recognize multiple barcodes simultaneously from the scanning UI or from the gallery. Scanning results will be returned as a list and during the scanning, the scanned barcodes will be caught by rectangles and their values will be shown on the scanning UI. In Multiprocessor Camera mode, you do not need to worry about developing the scanning process or camera control. Scan Kit will do all these tasks for you. However, you will need to customize the scanning UI according to the customization options that Flutter Scan Plugin provides.
In this article, we will learn Customized view in Pygmy collection application.
How to integrate Huawei Scan Kit in Android finance application?
Follow the steps.
1. Configure application on the AGC.
2. Client application development process.
Configure application on the AGC
Follow the steps.
Step 1: We need to register as a developer account in AppGallery Connect. If you are already developer ignore this step.
Step 2: Create an app by referring to Creating a Project and Creating an App in the Project
Step 3: Set the data storage location based on current location.
Step 4: Enabling Scan Kit. Project setting > Manage API > Enable Scan kit toggle button.
Step 5: Generating a Signing Certificate Fingerprint.
Step 6: Configuring the Signing Certificate Fingerprint.
Step 7: Download your agconnect-services.json file, paste it into the app root directory.
Client application development process
Follow the steps.
Step 1: Create Android application in the Android studio (Any IDE which is your favorite)
Step 2: Add the App level gradle dependencies. Choose inside project Android > app > build.gradle.
Code:
apply plugin: 'com.android.application'
apply plugin: 'com.huawei.agconnect'
Code:
dependencies {
//Huawei Scan
implementation 'com.huawei.hms:scan:1.3.2.300'
}
Root level gradle dependencies.
Code:
maven { url 'https://developer.huawei.com/repo/' }
classpath 'com.huawei.agconnect:agcp:1.4.1.300'
Step 3: Add storage and camera permission in AndroidManifest.xml
XML:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
Step 4: Build Application.
OnClick of QR code Icon
Java:
scanQrCode.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//requestCamera();
requestPermission(DEFINED_CODE, DECODE);
}
});
Request runtime permission.
Java:
/**
* Apply for permissions.
*/
private void requestPermission(int requestCode, int mode) {
if (mode == DECODE) {
decodePermission(requestCode);
} else if (mode == GENERATE) {
generatePermission(requestCode);
}
}
/**
* Apply for permissions.
*/
private void decodePermission(int requestCode) {
ActivityCompat.requestPermissions(
this,
new String[]{Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE},
requestCode);
}
/**
* Apply for permissions.
*/
private void generatePermission(int requestCode) {
ActivityCompat.requestPermissions(
this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
requestCode);
}
After permission granting it will redirect to another activity DefinedActivity
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSION_REQUEST_CAMERA) {
if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startCamera();
} else {
Toast.makeText(this, "Camera Permission Denied", Toast.LENGTH_SHORT).show();
}
}
//Customized View Mode
if (requestCode == DEFINED_CODE) {
Intent intent = new Intent(this, DefinedActivity.class);
this.startActivityForResult(intent, REQUEST_CODE_DEFINE);
}
}
DefinedActivity.java
Java:
import androidx.appcompat.app.AppCompatActivity;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.os.Bundle;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.Window;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import com.huawei.hms.hmsscankit.OnLightVisibleCallBack;
import com.huawei.hms.hmsscankit.OnResultCallback;
import com.huawei.hms.hmsscankit.RemoteView;
import com.huawei.hms.hmsscankit.ScanUtil;
import com.huawei.hms.ml.scan.HmsScan;
import com.huawei.hms.ml.scan.HmsScanAnalyzerOptions;
import com.shea.pygmycollection.R;
import java.io.IOException;
public class DefinedActivity extends Activity {
private FrameLayout frameLayout;
private RemoteView remoteView;
private ImageView backBtn;
private ImageView imgBtn;
private ImageView flushBtn;
int mScreenWidth;
int mScreenHeight;
//The width and height of scan_view_finder is both 240 dp.
final int SCAN_FRAME_SIZE = 240;
private int[] img = {R.drawable.flashlight_on, R.drawable.flashlight_off};
private static final String TAG = "DefinedActivity";
//Declare the key. It is used to obtain the value returned from Scan Kit.
public static final String SCAN_RESULT = "scanResult";
public static final int REQUEST_CODE_PHOTO = 0X1113;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_defined);
// Bind the camera preview screen.
frameLayout = findViewById(R.id.rim);
//1. Obtain the screen density to calculate the viewfinder's rectangle.
DisplayMetrics dm = getResources().getDisplayMetrics();
float density = dm.density;
//2. Obtain the screen size.
mScreenWidth = getResources().getDisplayMetrics().widthPixels;
mScreenHeight = getResources().getDisplayMetrics().heightPixels;
int scanFrameSize = (int) (SCAN_FRAME_SIZE * density);
//3. Calculate the viewfinder's rectangle, which in the middle of the layout.
//Set the scanning area. (Optional. Rect can be null. If no settings are specified, it will be located in the middle of the layout.)
Rect rect = new Rect();
rect.left = mScreenWidth / 2 - scanFrameSize / 2;
rect.right = mScreenWidth / 2 + scanFrameSize / 2;
rect.top = mScreenHeight / 2 - scanFrameSize / 2;
rect.bottom = mScreenHeight / 2 + scanFrameSize / 2;
//Initialize the RemoteView instance, and set callback for the scanning result.
remoteView = new RemoteView.Builder().setContext(this).setBoundingBox(rect).setFormat(HmsScan.ALL_SCAN_TYPE).build();
// When the light is dim, this API is called back to display the flashlight switch.
flushBtn = findViewById(R.id.flush_btn);
remoteView.setOnLightVisibleCallback(new OnLightVisibleCallBack() {
@Override
public void onVisibleChanged(boolean visible) {
if(visible){
flushBtn.setVisibility(View.VISIBLE);
}
}
});
// Subscribe to the scanning result callback event.
remoteView.setOnResultCallback(new OnResultCallback() {
@Override
public void onResult(HmsScan[] result) {
//Check the result.
if (result != null && result.length > 0 && result[0] != null && !TextUtils.isEmpty(result[0].getOriginalValue())) {
Intent intent = new Intent();
intent.putExtra(SCAN_RESULT, result[0]);
setResult(RESULT_OK, intent);
DefinedActivity.this.finish();
}
}
});
// Load the customized view to the activity.
remoteView.onCreate(savedInstanceState);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
frameLayout.addView(remoteView, params);
// Set the back, photo scanning, and flashlight operations.
backBtn = findViewById(R.id.back_img);
backBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
DefinedActivity.this.finish();
}
});
//setBackOperation();
setPictureScanOperation();
setFlashOperation();
}
/**
* Call the lifecycle management method of the remoteView activity.
*/
private void setPictureScanOperation() {
imgBtn = findViewById(R.id.img_btn);
imgBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent pickIntent = new Intent(Intent.ACTION_PICK,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
pickIntent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
DefinedActivity.this.startActivityForResult(pickIntent, REQUEST_CODE_PHOTO);
}
});
}
private void setFlashOperation() {
flushBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (remoteView.getLightStatus()) {
remoteView.switchLight();
flushBtn.setImageResource(img[1]);
} else {
remoteView.switchLight();
flushBtn.setImageResource(img[0]);
}
}
});
}
private void setBackOperation() {
backBtn = findViewById(R.id.back_img);
backBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
DefinedActivity.this.finish();
}
});
}
/**
* Call the lifecycle management method of the remoteView activity.
*/
@Override
protected void onStart() {
super.onStart();
remoteView.onStart();
}
@Override
protected void onResume() {
super.onResume();
remoteView.onResume();
}
@Override
protected void onPause() {
super.onPause();
remoteView.onPause();
}
@Override
protected void onDestroy() {
super.onDestroy();
remoteView.onDestroy();
}
@Override
protected void onStop() {
super.onStop();
remoteView.onStop();
}
/**
* Handle the return results from the album.
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_PHOTO) {
try {
Bitmap bitmap = MediaStore.Images.Media.getBitmap(this.getContentResolver(), data.getData());
HmsScan[] hmsScans = ScanUtil.decodeWithBitmap(DefinedActivity.this, bitmap, new HmsScanAnalyzerOptions.Creator().setPhotoMode(true).create());
if (hmsScans != null && hmsScans.length > 0 && hmsScans[0] != null && !TextUtils.isEmpty(hmsScans[0].getOriginalValue())) {
Intent intent = new Intent();
intent.putExtra(SCAN_RESULT, hmsScans[0]);
setResult(RESULT_OK, intent);
DefinedActivity.this.finish();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
activity_defined.xml
XML:
<?xml version="1.0" encoding="UTF-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:my_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/rim"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="200dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/flush_btn"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:visibility="gone"
android:gravity="center"
android:src="@drawable/flashlight_off" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:alpha="0.1"
android:background="#FF000000" />
<TextView
android:layout_above="@id/scan_area"
android:layout_marginBottom="10dp"
android:layout_centerHorizontal="true"
android:text="@string/scan_tip"
android:textAllCaps="false"
android:textColor="#FFFFFF"
android:textSize="15sp"
android:textStyle="bold"
android:layout_height="20dp"
android:layout_width="220dp" />
<ImageView
android:id="@+id/scan_area"
android:layout_width="240dp"
android:layout_height="240dp"
android:layout_centerInParent="true"
android:layout_centerHorizontal="true"
android:background="@drawable/cloors" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical"
android:background="@color/colorPrimary">
<TextView
android:layout_marginStart="10sp"
android:layout_toEndOf="@+id/back_img"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="@string/title"
android:textAllCaps="false"
android:textColor="#FFFFFF"
android:textSize="20sp"
android:textStyle="bold" />
<ImageView
android:id="@+id/back_img"
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="12dp"
android:layout_alignParentStart="true"
android:layout_marginStart="12dp"
android:layout_marginTop="4dp"
android:gravity="center"
android:src="@drawable/back" />
<ImageView
android:id="@+id/img_btn"
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="12dp"
android:layout_alignParentEnd="true"
android:layout_marginEnd="12dp"
android:layout_marginTop="4dp"
android:gravity="center"
android:src="@drawable/photo" />
</RelativeLayout>
</FrameLayout>
OnActvityResult of first screen REQUEST_CODE_DEFINE returns set the respected account details to screen.
Java:
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
IntentResult intentResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
if (intentResult != null) {
if (intentResult.getContents() == null) {
//textView.setText(“Cancelled”);
Toast.makeText(this, "Cancelled", Toast.LENGTH_SHORT).show();
} else {
//textView.setText(intentResult.getContents());
CollectionModel collectionModel = new Gson().fromJson(intentResult.getContents(), CollectionModel.class);
if (collectionModel != null && collectionModel.getIsPygmyApp().equals("1")) {
updateUi(collectionModel);
} else {
Toast.makeText(this, "Invalid QR Code", Toast.LENGTH_SHORT).show();
}
}
}
if (requestCode == REQUEST_CODE_DEFINE && data != null) {
HmsScan obj = data.getParcelableExtra(DefinedActivity.SCAN_RESULT);
if (obj != null) {
CollectionModel collectionModel = new Gson().fromJson(obj.getOriginalValue(), CollectionModel.class);
if (collectionModel != null && collectionModel.getIsPygmyApp().equals("1")) {
updateUi(collectionModel);
} else {
Toast.makeText(this, "Invalid QR Code", Toast.LENGTH_SHORT).show();
}
Log.e("data: ", new Gson().toJson(obj));
}
} else {
Toast.makeText(this, "Cancelled", Toast.LENGTH_SHORT).show();
}
super.onActivityResult(requestCode, resultCode, data);
}
ResultGenerating QR Code
Scanning QR Code
Tips and Tricks
Make sure you are already registered as Huawei developer.
Make sure you have already downloaded service.agconnect.json and added it to app folder.
Make sure all the dependencies are added.
Do not forget to add the camera and storage permission.
If you are running android version 6 or later, follow the runtime permission rule.
ConclusionIn this article, we have learnt how to integrate Scan kit in Android. We have learnt the types of scan available. And we have learnt how to use the Customized view. Collecting cash using the QR code in each shop makes agent life easy. In upcoming article I’ll come up with new article.
ReferenceScan kit