Android Custom View make gorgeous verification code _android

Source: Internet
Author: User
Tags abs drawtext

No more nonsense to say, first show you the custom view effect map, if you think it is good, please continue to read.

How, this kind of verification code is not very common, the following we do it ourselves to achieve this effect, do-it-yourself, ample, ha ~

First, customize the view steps

Custom view has always been considered an android step to the master of the way, in fact, custom view is very simple, custom view is really difficult to draw a highly difficult graphics, which requires a good math skills (regret not to learn maths well), Because drawing the graph often needs to compute the coordinate point and the similar geometric transformation and so on. Custom view typically requires only the following steps:

Write a class to inherit the view class;

The method of constructing a new view;

Measure the size of the view, which is to rewrite the Onmeasure () method;

Re-OnDraw () method.

The third step is not necessary, and only when the system cannot determine the size of the custom view requires ourselves to rewrite the Onmeasure () method to complete the custom view size measurement. Because if the user (programmer) does not specify its exact size (width or height) when using our custom view, such as: The Layout_width or Layout_heigth attribute value in the layout file is wrap_content instead of Match_ Parent or an exact value, then the system does not know the size of the graphics we have customized view to draw in OnDraw (), so we usually have to customize view support WRAP_ Content then we have to rewrite the Onmeasure method to tell the system the size (width and height) of the view that we want to draw.

Also, if we need some special attributes for our custom view, we also need custom attributes, and this article will cover the custom attributes and the four steps above.

Implementation of Custom View

To implement this validation code control, we need to analyze how it will be implemented. By looking at the above effect chart, we can know that to achieve this effect, we first need to draw the verification code string, that is, the text part of the diagram, and then draw some interference points, and then draw the interference line, analysis finished. Below we follow the analysis results step-by-step to achieve this effect.

1. Inherit view, rewrite the construction method

Write a class to inherit view and then recreate its method of constructing

/**
* Created by Lt on 2016/3/2.
* * Public
class Validationcode extends view{
/**
* Called when creating View in Java code, that is, new
* @param
Context * * Public
Validationcode {This
(context,null);
}

/** *
Call
* @param context
* @param attrs/Public when
using view in an XML layout file but not specifying style
Validationcode (context, AttributeSet attrs) {This
(context, attrs, 0);
}

/** *
Call
* @param context
* @param attrs
* @param defstyleattr
*/< When using view in an XML layout file and specifying style C26/>public Validationcode (context, AttributeSet attrs, int defstyleattr) {
Super (context, attrs, DEFSTYLEATTR);
Do some initialization work
init ();
}

View has three construction methods, and the general practice is to have one parameter and two parameters of the constructor method to call the three constructor parameters of the method, the three construction methods of the invocation of the method to see the comments above. In this construction method we first do some initialization random authentication code strings, brushes and so on work:

/**
* Initialize some data *
/private void init () {
//Generate random numbers and letter combinations
mcodestring = GETCHARANDNUMR (mcodecount);
Initializes a text brush
mtextpaint = new Paint ();
Mtextpaint.setstrokewidth (3); The brush size is 3
mtextpaint.settextsize (mtextsize);//Set Text size
//Initialize interference point Brush
mpointpaint = new Paint ();
Mpointpaint.setstrokewidth (6);
Mpointpaint.setstrokecap (Paint.Cap.ROUND); Set the breakpoint at the Circle
//initialization Interference line brush
mpathpaint = new Paint ();
Mpathpaint.setstrokewidth (5);
Mpathpaint.setcolor (Color.gray);
Mpathpaint.setstyle (Paint.Style.STROKE); Set the brush to a hollow
mpathpaint.setstrokecap (Paint.Cap.ROUND);//Set
the width value of the circle//get the display of the validation code string at the breakpoint
mtextwidth = Mtextpaint.measuretext (mcodestring);
}

Here, we have completed the first two steps in the custom view step, followed by the third step, which is to rewrite onmeasure () to measure our custom view size (width and height):

2. Rewrite onmeasure () to complete the View size measurement

/**
* To support wrap_content like Layout_width and Layout_height attributes must be renewed this method
* @param widthmeasurespec
* @param Heightmeasurespec
*
/@Override protected void onmeasure (int widthmeasurespec, int heightmeasurespec) {
//measure the width and height of the control, basically the template method
int measurewidth = Measurewidth (widthmeasurespec);
int measureheight = Measureheight (heightmeasurespec);
In fact, this method will eventually call setmeasureddimension (int measurewidth,int measureheight);
The measured width and height are set in to complete the measurement
setmeasureddimension (measurewidth, measureheight);
}

Method of measuring width:

/**
* Measuring width
* @param widthmeasurespec * *
private int measurewidth (int widthmeasurespec) {
int result = (int) (mtextwidth*1.8f);
int widthmode = Measurespec.getmode (widthmeasurespec);
int widthsize = measurespec.getsize (widthmeasurespec);
if (Widthmode = = measurespec.exactly) {
//precision measurement mode, that is, layout_width or layout_height in the layout file is generally an exact value or Match_parent
result = Widthsize; Since it is a precise pattern, the width of the measurement can be directly returned
}else{
if (Widthmode = = measurespec.at_most) {
//MAX mode, that is, the layout file Layout_ Width or layout_height is generally wrap_content result
= Math.min (result,widthsize);
}
return result;
}

Method of measuring height:

/**
* Measuring height
* @param heightmeasurespec * *
private int measureheight (int heightmeasurespec) {
int result = (int) (mtextwidth/1.6f);
int heightmode = Measurespec.getmode (heightmeasurespec);
int heightsize = measurespec.getsize (heightmeasurespec);
if (Heightmode = = measurespec.exactly) {
//precision measurement mode, that is, layout_width or layout_height in the layout file is generally an exact value or Match_parent
result = Heightsize; Since it is a precise pattern, the width of the measurement can be directly returned
}else{
if (Heightmode = = measurespec.at_most) {
//MAX mode, that is, the layout file Layout_ Width or layout_height is generally wrap_content result
= Math.min (result,heightsize);
}
return result;
}

Description: In fact, the Onmeasure () method will eventually call the setmeasureddimension (int measurewidth,int measureheight), the measured width and height will be set to complete the measurement, And all we have to do is measure the width and height of the value, measure the width and height of the method is to get when the user (programmer) does not give our control to specify the exact value (specific values or match_parent) when the appropriate width and height, so, The above method of measuring width and height is basically a template method to do is to get a proper value of result, where we do not need to pay attention to the value of result, because this value based on the control of a suitable value (perhaps not very appropriate).

When you've finished measuring the controls, then we're going to finish the step of drawing the control, which is the step of customizing the core of the view to rewrite the OnDraw () method to draw the graph.

3. Rewrite OnDraw (), Draw graphics

According to our analysis above, we need to draw the Captcha text string, the jamming point, the jamming line. Because the jamming point and the jamming line need coordinates and path to draw, do some initialization random jamming point coordinates and the disturbing line path before drawing:

private void InitData () {
//Gets the width and height of the control, which has been measured at this time
mheight = GetHeight ();
Mwidth = GetWidth ();
Mpoints.clear ();
Generate the interference point coordinate for
(int i=0;i<150;i++) {
PointF PointF = new PointF (Mrandom.nextint (mwidth) +10,mrandom.nextint ( Mheight) +10);
Mpoints.add (PointF);
}
Mpaths.clear ();
Generate interference line coordinates for
(int i=0;i<2;i++) {
path Path = new Path ();
int startx = Mrandom.nextint (MWIDTH/3) +10;
int starty = Mrandom.nextint (MHEIGHT/3) +10;
int endx = Mrandom.nextint (MWIDTH/2) +mwidth/2-10;
int EndY = Mrandom.nextint (MHEIGHT/2) +mheight/2-10;
Path.moveto (startx,starty);
Path.quadto (Math.Abs (ENDX-STARTX)/2,math.abs (endy-starty)/2,endx,endy);
Mpaths.add (path);
}

With this data, we can start drawing the graph.

(1) Draw the Captcha text string

Because the captcha text string is randomly generated, we need to use code to randomly generate this random captcha:

/**
* Java generates random numbers and letter combinations
* @param length[Generate random number of lengths]
* @return
/public static String GETCHARANDNUMR ( int length) {
String val = "";
Random Random = new Random ();
for (int i = 0; i < length; i++) {
//output letter or numeric
String charornum = random.nextint (2)% 2 = 0? "Char": "num";
String
if ("char". Equalsignorecase (Charornum)) {
//Get uppercase or lowercase
int choice = random.nextint (2)% 2 = 0? 65:97;
Val + = (char) (choice + random.nextint);
} else if ("num". Equalsignorecase (Charornum)) {//number
val = string.valueof (random.nextint);
}
return val;
}

This code is the Java Foundation, I believe everyone can read, do not understand that it does not matter, this code on the Internet to have a random search, in fact, I also directly from the Internet search, hey.

The Android 2D Graphics API Canvas provides a drawxxx () method for drawing a variety of graphics, with DrawText () methods to draw text, and Drawpostext () to draw text at a given coordinate point. Drawtextonpath () Draws a graph on a given path. Careful observation of the above effect chart, found that some of the text is not horizontal, that is, some are tilted, which can give us a certain degree of verification code to enhance the difficulty of recognition, to achieve the text tilt effect, we can through the Drawtextonpath () in a given path to draw text to achieve tilt effect, However, this method is difficult to implement (coordinate points and paths are difficult to calculate), so we can use the canvas provided by the Position transformation Method Rorate () combined with drawtext () to achieve text skew effect.

int length = Mcodestring.length ();
float charlength = mtextwidth/length;
for (int i=1;i<=length;i++) {
int offsetdegree = mrandom.nextint;
Here will only produce 0 and 1, if it is 1 so positive rotation angle, otherwise rotating negative angle
Offsetdegree = mrandom.nextint (2) = = 1?offsetdegree:-offsetdegree;
Canvas.save ();
Canvas.rotate (Offsetdegree, MWIDTH/2, MHEIGHT/2);
Set a random color for the brush, +20 to remove some boundary values
Mtextpaint.setargb (255, Mrandom.nextint +, Mrandom.nextint (200) + 20, Mrandom.nextint (+); Canvas.drawtext (String.valueof (Mcodestring.charat (I-1)), (i-1) * Charlength * 1.6f+30, Mheight * 2/3f, mtextpaint);
Canvas.restore ();
}

This code draws each character in the verification code string separately through a for loop, rotates a random positive and negative angle on the canvas each time a character is drawn, and then draws the character by the DrawText () method, each character's drawing start coordinates vary according to the length and position of the character, which is calculated by itself, It may not be the right place. Note that every time you change the position of the canvas canvas, you call the Canvas.save () method to save the previously drawn shape, and then call Canvas.restore () to restore the canvas's position when the drawing is finished. So that the next time you draw the graphic, it will not be affected by the position of the previous canvas.

(2) Draw the jamming point

Interference effect 1--interference point
for (PointF pointf:mpoints) {
Mpointpaint.setargb (255,mrandom.nextint (200) +20, Mrandom.nextint (+20,mrandom.nextint) +20);
Canvas.drawpoint (Pointf.x,pointf.y,mpointpaint);
}

A random color is set for the jamming point brush, and the Canvas.drawpoint () is used to draw points according to the coordinates of the randomly generated points.

(3) Draw the jamming line

Interference effect 2--interference line
for (Path path:mpaths) {
Mpathpaint.setargb (255, Mrandom.nextint () +, Mrandom.nextint ( (mrandom.nextint) +);
Canvas.drawpath (path, mpathpaint);

A random color is set for the interference line brush, then the Bezier curve is drawn using Canvas.drawpath () according to the random generation path, thereby drawing out the interference line.

4. Rewrite ontouchevent, customizing view events

This is done here to accomplish some action when we click on our custom view, that is, customizing the View event. Here, we need to change the text string of the verification code when the user clicks on the validation code control.

@Override Public
Boolean ontouchevent (Motionevent event) {
switch (event.getaction ()) {
case Motionevent.action_down:
//Regenerate random numbers and letter combinations
mcodestring = GETCHARANDNUMR (mcodecount);
Invalidate ();
break;
Default: Break
;
}
Return Super.ontouchevent (event);
}

OK, here's our custom view that's basically done, and maybe people will ask, is this custom view too bad extensibility, too low customization, and good custom attributes? Where did it go? Don't worry, we'll come from the properties that define our own view, custom attributes.

5. Customizing properties to improve customization of custom view

(1) Define our attributes (set) in the Attrs.xml file of the resource file

<?xml version= "1.0" encoding= "Utf-8"?>
<resources>
<declare-styleable name= " Indentifyingcode ">
<attr name=" Codecount "format=" integer|reference "></attr>
<attr Name= "Textsize" format= "Dimension" ></attr>
</declare-styleable>
</resources>

Description

Defining our attributes in the attr node in the Attrs.xml file requires the name attribute to represent our property value, and the Format property to represent the value of the property, in many ways, and if the attribute value can be in multiple formats, the format is "|" Separate

The declare-styleable node is used to define our custom set of properties, whose Name property specifies the names of the property set, which can be arbitrary, but generally the name of the custom control;

If the attribute is already defined (such as layout_width), then you can refer to the property directly and do not specify a format.

(2) referencing a custom attribute in a layout file, noting that namespaces need to be introduced

<relativelayout xmlns:android= "http://schemas.android.com/apk/res/android" xmlns:tools= "http:// Schemas.android.com/tools "xmlns:lt=" Http://schemas.android.com/apk/res-auto "
android:layout_width=" Match_ Parent "
android:layout_height=" match_parent ">
<com.lt.identifyingcode.validationcode
android : id= "@+id/validationcode"
android:layout_width= "wrap_content"
android:layout_centerinparent= "true"
lt:textsize= "25SP"
android:background= "@android: Color/darker_gray"
android:layout_height= "Wrap_ Content "/>
</RelativeLayout>

The introduction of namespaces can only be done now by adding xmlns:lt= "Http://schemas.android.com/apk/res-auto" (lt to your own namespace name), and before introducing a namespace method for xmlns:custom= " Http://schemas.android.com/apk/res/com.example.customview01 ", the package path behind Res refers to the project's package '

(3) Get the value of the custom property in the constructor method

TypedArray TypedArray = context.obtainstyledattributes (Attrs, r.styleable.indentifyingcode);
Mcodecount = Typedarray.getinteger (R.styleable.indentifyingcode_codecount, 5); Gets the validation code number attribute value in the layout, defaults to 5
//Get the size of the captcha text in the layout, default to 20sp
mtextsize = typedarray.getdimension ( R.styleable.indentifyingcode_textsize, Typedarray.getdimensionpixelsize (R.styleable.indentifyingcode_textsize, int) typedvalue.applydimension (typedvalue.complex_unit_sp, Getresources (). Getdisplaymetrics ()));
A good habit is to use the resources to remember to recycle, you want to open the database and IO flow after use to remember to close the same
typedarray.recycle ();

OK, the custom attribute is also completed, the value is also obtained, then we only need to put the custom attribute value in our OnDraw () when we use the line, the custom attribute is so simple ~, see here, perhaps a little confusing, look at the complete code to tidy up.

Package Com.lt.identifyingcode;
Import Android.content.Context;
Import Android.content.res.TypedArray;
Import Android.graphics.Canvas;
Import Android.graphics.Color;
Import Android.graphics.Paint;
Import Android.graphics.Path;
Import Android.graphics.PointF;
Import Android.util.AttributeSet;
Import Android.util.TypedValue;
Import android.view.MotionEvent;
Import Android.view.View;
Import java.util.ArrayList;
Import Java.util.Random;
/** * Created by Lt on 2016/3/2. * * public class Validationcode extends view{/** * The width of the control/private int mwidth;/** * control's height/private int mheight;/** * Verification Code Text Brush * * Private Paint mtextpaint;
Text Brush/** * Interference Point coordinate set * * Private arraylist<pointf> mpoints = new arraylist<pointf> ();
Private Random mrandom = new Random ();;
/** * Interference Point Brush * * Private Paint mpointpaint;
/** * Draw a set of paths for Bezier curves/private arraylist<path> mpaths = new arraylist<path> ();
/** * Interference Line Brush * * Private Paint mpathpaint;
/** * Verification Code String */private string mcodestring; /** * Verification code number of digits */private int mcodecount;
/** * Verification Code character Size * * Private float mtextsize;
/** * Verification Code string Display width * * private float mtextwidth; /** * Call when creating view in Java code, that is, new * @param context */public Validationcode (context) {this (context,null);}/** * in X  Call * @param context * @param attrs/Public validationcode when using view but not specifying style in ml layout file (context, AttributeSet attrs)
{This (context, attrs, 0);}/** * Call * @param context * @param attrs * @param defstyleattr * When using view in an XML layout file and specifying style Public Validationcode (context, AttributeSet attrs, int defstyleattr) {Super (context, attrs, defstyleattr); getAt
Trvalues (context, attrs);
Do some initialization work init (); /** * Gets the value in the layout file * @param context */private void Getattrvalues (context Context,attributeset attrs) {TypedArray Typedarr
ay = context.obtainstyledattributes (attrs, R.styleable.indentifyingcode); Mcodecount = Typedarray.getinteger (R.styleable.indentifyingcode_codecount, 5); Gets the validation code number attribute value in the layout, defaults to 5//Get the size of the captcha text in the layout, default to 20sp mtextsize = Typedarray.getdiMension (R.styleable.indentifyingcode_textsize, Typedarray.getdimensionpixelsize (R.styleable.IndentifyingCode_
TEXTSIZE, (int) typedvalue.applydimension (typedvalue.complex_unit_sp, Getresources (). Getdisplaymetrics ()));
A good habit is to use the resources to remember to recycle, you want to open the database and IO flow after use to remember to close the same typedarray.recycle (); /** * To support wrap_content like Layout_width and Layout_height attributes must be renewed this way * @param widthmeasurespec * @param heightmeasurespec * * @Ov Erride protected void onmeasure (int widthmeasurespec, int heightmeasurespec) {//measure the width and height of the control, basically the template method int measurewidth =
Measurewidth (WIDTHMEASURESPEC);
int measureheight = Measureheight (Heightmeasurespec);
In fact, this method will eventually call setmeasureddimension (int measurewidth,int measureheight);
The measured width and height are set in to complete the measurement setmeasureddimension (measurewidth, measureheight); @Override protected void OnDraw (Canvas Canvas) {//Initialization data initdata (); int length = Mcodestring.length (); float charlengt
h = mtextwidth/length; for (int i=1;i<=length;i++) {int offsetdegree = Mrandom.nextint (15);//here will only produce 0 and 1, if 1 is so positive rotation angle,Otherwise rotate negative angle Offsetdegree = mrandom.nextint (2) = = 1?offsetdegree:-offsetdegree;
Canvas.save ();
Canvas.rotate (Offsetdegree, MWIDTH/2, MHEIGHT/2);
Set a random color to the brush Mtextpaint.setargb (255, Mrandom.nextint +, mrandom.nextint () +, Mrandom.nextint (200) + 20);
Canvas.drawtext (String.valueof (Mcodestring.charat (I-1)), (i-1) * charlength * 1.6f+30, Mheight * 2/3f, MTextPaint);
Canvas.restore (); //Interference effect 1--interference point for (PointF pointf:mpoints) {Mpointpaint.setargb (255,mrandom.nextint) +20,mrandom.nextint (200) +
20,mrandom.nextint (200) +20);
Canvas.drawpoint (Pointf.x,pointf.y,mpointpaint);  //Interference effect 2--interference line for (Path path:mpaths) {Mpathpaint.setargb (255, Mrandom.nextint +, Mrandom.nextint (200) + 20,
Mrandom.nextint (200) + 20);
Canvas.drawpath (path, mpathpaint); 
The private void InitData () {//Gets the width and height of the control, which has been measured at this time Mheight = GetHeight (); mwidth = GetWidth (); Mpoints.clear ();//Generate interference point coordinates for (int i=0;i<150;i++) {PointF PointF = new PointF (Mrandom.nextint (mwidth) +10,mrandom.nextInt (mheight) +10);
Mpoints.add (PointF);
} mpaths.clear (); Generate interference line coordinates for (int i=0;i<2;i++) {Path PATH = new Path (); int startx = Mrandom.nextint (MWIDTH/3) +10; int starty = Mrando
M.nextint (MHEIGHT/3) +10;
int endx = Mrandom.nextint (MWIDTH/2) +mwidth/2-10;
int EndY = Mrandom.nextint (MHEIGHT/2) +mheight/2-10;
Path.moveto (Startx,starty);
Path.quadto (Math.Abs (ENDX-STARTX)/2,math.abs (endy-starty)/2,endx,endy);
Mpaths.add (path); }/** * Initialize some data/private void init () {//Generate random number and letter combination mcodestring = GETCHARANDNUMR (Mcodecount);//Initialize text brush mtextpaint
= new Paint (); Mtextpaint.setstrokewidth (3); The brush size is 3 mtextpaint.settextsize (mtextsize);
Sets the text size//initialization interference point brush Mpointpaint = new Paint ();
Mpointpaint.setstrokewidth (6); Mpointpaint.setstrokecap (Paint.Cap.ROUND);
Set the breakpoint at the circle//initialization interference line brush Mpathpaint = new Paint ();
Mpathpaint.setstrokewidth (5);
Mpathpaint.setcolor (Color.gray); Mpathpaint.setstyle (Paint.Style.STROKE); Set the brush to a hollow mpathpaint.setstrokecap (Paint.Cap.ROUND); Sets the width value displayed for the circle//Get Authenticode string at the breakpoint MtexTwidth = Mtextpaint.measuretext (mcodestring); /** * Java generates random numbers and letters combination * @param length[generate random number of lengths] * @return/public static String getcharandnumr (int length) {string VA
L = "";
Random Random = new Random (); for (int i = 0; i < length; i++) {//output letter or numeric String charornum = random.nextint (2)% 2 = 0?
"Char": "num";  String if ("char". Equalsignorecase (Charornum)) {//get uppercase or lowercase int choice = random.nextint (2)% 2 = 0? 65:97; val + = (char)
(Choice + random.nextint (26));
else if ("num". Equalsignorecase (Charornum)) {//number val + = string.valueof (Random.nextint (10));}
return Val;  @Override public boolean ontouchevent (Motionevent event) {switch (event.getaction ()) {case Motionevent.action_down://
Regenerate random numbers and letter combinations mcodestring = GETCHARANDNUMR (Mcodecount);
Invalidate ();
Break
Default:break;
Return Super.ontouchevent (event); /** * Measuring Width * @param widthmeasurespec * * * private int measurewidth (int widthmeasurespec) {Int. result = (int) (mtextwidth*
1.8f); int widthmode = MeasurespeC.getmode (WIDTHMEASURESPEC);
int widthsize = measurespec.getsize (Widthmeasurespec); if (Widthmode = = measurespec.exactly) {//Precision measurement mode, that is, layout_width or layout_height in the layout file is generally an exact value or match_parent result = Widthsize; Since it is a precise pattern, the width of the measurement can be directly returned}else{if (Widthmode = = Measurespec.at_most) {//MAX mode, that is, layout_width or layout_ in the layout file
Height is generally wrap_content result = Math.min (result,widthsize);
} return result; /** * Measuring Height * @param heightmeasurespec * * * private int measureheight (int heightmeasurespec) {Int. result = (int) (Mtextwid
th/1.6f);
int heightmode = Measurespec.getmode (Heightmeasurespec);
int heightsize = measurespec.getsize (Heightmeasurespec); if (Heightmode = = measurespec.exactly) {//Precision measurement mode, that is, layout_width or layout_height in the layout file is generally an exact value or match_parent result = Heightsize; Since it is a precise pattern, the width of the measurement can be directly returned}else{if (Heightmode = = Measurespec.at_most) {//MAX mode, that is, layout_width or layout_ in the layout file
Height is generally wrap_content result = Math.min (result,heightsize);
} return result; /** * Get the verification code string, the match can only be a string comparison (the specific rules of comparison to determine their own) * @return Verification Code String */public string getcodestring () {return mcodestring}}} 

Summary: Here is not so much a custom view to draw graphics, the key is the calculation of coordinate points, here in the calculation of the coordinates may not be very good, the above is to share the Android custom view production Gorgeous verification code, I hope to help you! We have any good ideas or suggestions wish to leave a message to tell me, grateful ~.

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.