Android RoboGuice 使用指南1

转载于:http://www.imobilebbs.com/wordpress/archives/2480


Android RoboGuice 使用指南(1):概述

在开发应用时一个基本原则是模块化,并且近最大可能性地降低模块之间的耦合性。在Java平台上Spring Framework 以及.Net 平台 CAB ,SCSFPrism (WPF,Silverlight)中都有对Dependency injection的支持。

Dependency injection 大大降低了类之间的依赖性,可以通过annotation (Java)或是SeviceDepdendcy (.Net) 描述类之间的依赖性,避免了直接调用类似的构造函数或是使用Factory来参加所需的类,从而降低类或模块之间的耦合性,以提高代码重用并增强代码的可维护性。

Google Guice提供了Java平台上一个轻量级的 Dependency injection 框架,并可以支持开发Android应用。本指南将使用Android平台来说明Google Guice的用法。

简单的来说:Guice 降低了Java代码中使用 new 和 Factory函数的调用。可以把Guice 的@Inject 看作 new 的一个替代品。使用Guice可能还需要写一些Factory方法,但你的代码不会依赖这些Factory方法来创建实例。 使用Guice 修改代码,单元测试已经代码重用变得更容易。

RoboGuice 为Android平台上基于Google Guice开发的一个库,可以大大简化Android应用开发的代码和一些繁琐重复的代码。比如代码中可能需要大量使用findViewById在XML中查找一个View,并将其强制转换到所需类型,onCreate 中可能有大量的类似代码。RoboGuice 允许使用annotation 的方式来描述id于View之间的关系,其余的工作由roboGuice库来完成。比如:

1 classAndroidWayextendsActivity {
2 TextView name;
3 ImageView thumbnail;
4 LocationManager loc;
5 Drawable icon;
6 String myName;
7  
8 publicvoidonCreate(Bundle savedInstanceState) {
9 super.onCreate(savedInstanceState);
10 setContentView(R.layout.main);
11 name = (TextView) findViewById(R.id.name);
12 thumbnail = (ImageView) findViewById(R.id.thumbnail);
13 loc = (LocationManager) getSystemService(Activity.LOCATION_SERVICE);
14 icon = getResources().getDrawable(R.drawable.icon);
15 myName = getString(R.string.app_name);
16 name.setText("Hello, " + myName );
17 }
18 }

如果使用roboguice 来写:

1 classRoboWayextendsRoboActivity {
2 @InjectView(R.id.name) TextView name;
3 @InjectView(R.id.thumbnail) ImageView thumbnail;
4 @InjectResource(R.drawable.icon) Drawable icon;
5 @InjectResource(R.string.app_name) String myName;
6 @InjectLocationManager loc;
7  
8 publicvoidonCreate(Bundle savedInstanceState) {
9 super.onCreate(savedInstanceState);
10 setContentView(R.layout.main);
11 name.setText("Hello, " + myName );
12 }
13 }

只需使用@InjectView 来描述 view 和Id之间的关系,RoboGuice 自动完成余下的工作,代码简洁易读。在介绍完Google Guice ,再接着介绍RoboGuice 在Android平台上使用方法。

Android RoboGuice 使用指南(2):第一个例子Hello World

首先介绍一下如果将Guice 和RoboGuice 的库添加到项目中。

  1. 下载RoboGuiceguice-2.0-no_aop.jar(not guice-3.0),或者下载
  2. 创建一个新Android项目,比如GuiceDemo,目标平台Android1.5以上。
  3. 一般可以在该项目下添加一个lib目录,将两个jar文件拷到lib目录下,然后通过: Project > Properties > Java Build Path > Libraries > Add External JARs

添加了对应guice 和roboguice库的引用之后,就可以开始编写第一个使用roboguice 的例子。

使用roboguice 的步骤:

1. 创建一个RoboApplication 的子类GuiceApplication,GuiceApplication为Appliacation的子类,因此需要修改AndroidManifest.xml,将Application 的name 指向这个类。可以参见Android简明开发教程九:创建应用程序框架

<application android:name=”GuiceApplication”
android:icon=”@drawable/icon” android:label=”@string/app_name”>
<activity android:name=”.GuiceDemo”
android:label=”@string/app_name”>
<intent-filter>
<action android:name=”android.intent.action.MAIN” />
<category android:name=”android.intent.category.LAUNCHER” />
</intent-filter>
</activity>

</application>

2. 在这个简单的例子中,它使用的Layout 定义如下:

<?xml version=”1.0″ encoding=”utf-8″?>
<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:orientation=”vertical”
android:layout_width=”fill_parent”
android:layout_height=”fill_parent”
>
<TextView
android:id=”@+id/hello”
android:layout_width=”fill_parent”
android:layout_height=”wrap_content”
android:text=”@string/hello”
/>
</LinearLayout>

我们定义了一个TextView ,它的id为hello.

假定这个应用使用一个IGreetingService ,它有一个方法getGreeting() 返回一个字符串,至于IGreetingService 如何实现,GuideDemo 不需要关心。

Dependency injection 设计模式的一个核心原则为: Separate behavior from dependency resolution. 也就说将应用需要实现的功能和其所依赖的服务或其它对象分离。 对本例来说GuiceDemo只要知道它依赖于IGreetingService 服务,至于IGreetingService有谁实现GuiceDemo并不需要知道。

在Roboguice 中使用@Inject 来表示这种依赖关系。

1 publicclassGuiceDemo extendsRoboActivity {
2  
3 @InjectView(R.id.hello) TextView helloLabel;
4 @InjectIGreetingService greetingServce;
5  
6 @Override
7 publicvoidonCreate(Bundle savedInstanceState) {
8 super.onCreate(savedInstanceState);
9 setContentView(R.layout.main);
10 helloLabel.setText(greetingServce.getGreetings());
11 }
12 }
  • 使用RoboGuice 的Activity需要从RoboActivity派生(RoboActivity为Activity的子类).
  • 使用@Inject标注greetingServce依赖于IGreetingService服务
  • 使用@InjectView表示helloLabel 依赖于R.id.hello (XML)

代码中没有创建greetingServce 对象的代码(如 new xxx()) 和为helloLabel 赋值的代码。这些值都可以Roboguice 自动创建和赋值注入(Inject)到变量中。

为了说明问题,我们在代码中添加两个对getGreetings的实现,一个为HelloWorld, 一个为HelloChina:

1 publicclassHelloChina implementsIGreetingService{
2  
3 @Override
4 publicString getGreetings() {
5 return"Hello,China";
6 }
7  
8 }
9  
10 publicclassHelloWorld implementsIGreetingService{
11  
12 @Override
13 publicString getGreetings() {
14 return"Hello,World";
15 }
16  
17 }

3. 到这里,你可能有些困惑,RoboGuice怎么知道使用那个类(HelloWorld或是HelloChina)为GuiceDemo中的greetingServce 赋值呢?这是通过在Module 中定义binding 来实现的。

在项目中添加一个GreetingModule (从AbstractAndroidModule 派生)重载configure方法:

1 publicclassGreetingModule extendsAbstractAndroidModule{
2  
3 @Override
4 protectedvoidconfigure() {
5 bind(IGreetingService.class).to(HelloWorld.class);
6 //bind(IGreetingService.class).to(HelloChina.class);
7  
8 }
9  
10 }

将IGreetingService 绑定到HelloWorld 类。

然后在GuiceApplication 的addApplicationModules 添加上述模块:

1 publicclassGuiceApplication extendsRoboApplication {
2  
3 protectedvoidaddApplicationModules(List<Module> modules) {
4 modules.add(newGreetingModule());
5 }
6  
7 }

可以将GreetingModule 绑定改为HelloChina ,对比一下:

通过改变binding ,GuiceDemo 显示了不同的结果,GuiceDemo不依赖于具体的实现,可以非常方便的改变接口的实现而无需更改GuiceDemo的代码。大大降低了类于类之间的耦合性。

后面将逐个介绍Guice和RoboGuice支持的Binding类型和用法(Guice)以及与android 平台相关的Dependency injection (RoboGuice)

本例下载(含 roboguice 库)

Android RoboGuice 使用指南(3):Bindings 概述

一个应用中类于类之间的依赖关系可能非常复杂,创建于个类实例,需要先创建类所依赖的类的示例,而创建所依赖类的实例,这些类又可能依赖其它类,以此类推。因此在创建一个类实例时,你正在需要创建的是一个对象图对象(Object Graph)。

手工创建Object Graph 是一个非常繁琐而且容易出错的过程,并且很难对代码进行测试,而Guice或Roboguice可以帮助你创建Object Graph,所要做的工作是配置类和类之间的依赖关系。

模块(Modules) 是Guice 构造Object Graph 的基本构造块,Guice中构造object Graph 的工作有被称为”Injector”的类来完成。

Guice在模块为AbstractMoudule 的子类,而RoboGuice在模块为AbstractAndroidModule的子类。RoboGuice利用 Injector 来创建所依赖的对象,而Injector 为参照Module 中定义的Bindings来构造类于类之间的关系图。

打个比方,如果你熟悉make file 或是其它Build 系统(如 wix) 。你使用makefile 定义好需编译的对象所依赖的源码文件,这些源码由可能依赖其它库或头文件等。makefile 定义的这些依赖关系对应到Roboguice 中为模块中定义的bindings 。

使用make 编译某个目标程序 (Target), make 会查看makefile 中的依赖关系,依次先编译被依赖的对象直到最终编译Target。对应到Roboguide(Guice)为Injector 创建某个对象,它会根据定义的Bindings 首先创建那些被依赖的对象,直到创建所需对象。

在HelloWorld例子中,我们没有看到Injector的直接使用,这是因为RoboGuice 替我们调用了Injector来创建IGreetingService对象。

如果在某些情况下,如果你想直接使用Injector ,可以使用RoboActivity 的getInjector().

比如修改GuiceDemo,去掉@Inject IGreetingService greetingServce 而使用Injector的getInstance 来创建IGreetingService 实例。

1 publicclassGuiceDemo extendsRoboActivity {
2  
3 @InjectView(R.id.hello) TextView helloLabel;
4 //@Inject IGreetingService greetingServce;
5  
6 @Override
7 publicvoidonCreate(Bundle savedInstanceState) {
8 super.onCreate(savedInstanceState);
9 setContentView(R.layout.main);
10  
11 Injector injector=getInjector();
12 IGreetingService greetingServce
13 =injector.getInstance(IGreetingService.class);
14 helloLabel.setText(greetingServce.getGreetings());
15 }
16  
17 }

Module中的还是绑定到HelloChina.

1 publicclassGreetingModule extendsAbstractAndroidModule{
2  
3 @Override
4 protectedvoidconfigure() {
5 //bind(IGreetingService.class).to(HelloWorld.class);
6 bind(IGreetingService.class).to(HelloChina.class);
7  
8 }
9  
10 }

Injector 的工作就是构造Object Graph,当你调用getInstance 来构造某个类型的对象时,Injector 会自动根据类之间的依赖关系创建所需类的实例。

定义类之间的依赖关系的方法是通过扩展AbstractAndroidModule,重载其configure方法。在configure方法中定义各种Bindings。这些方法同时也做类型检测,如果使用的类型不正确,编译器将给出错误。

绑定Bindings 可以有下面几种类型:

  • Linked bindings
  • instance bindings
  • @provider methods
  • provider bindings
  • constructor bindings
  • untargetted bindings
  • built-in bindings
  • just-in-time bindings
  • providers 等

后面就逐个介绍这些bindings ,这些bindings 是通用的和Android平台相关性不大,可以同时用于Java EE ,Java SE 平台,RoboGuice 提供了于Android平台相关的dependency injector ,后面也有详细介绍。

Android RoboGuice 使用指南(4):Linked Bindings

Roboguice 中最常用的一种绑定为Linked Bindings,将某个类型映射到其实现。这里我们使用引路蜂二维图形库中的类为例,引路蜂二维图形库的使用可以参见Android简明开发教程八:引路蜂二维图形绘制实例功能定义

使用下面几个类 IShape, Rectangle, MyRectangle, MySquare, 其继承关系如下图所示:

下面代码将IShape 映射到MyRectangle

1 publicclassGraphics2DModule extendsAbstractAndroidModule{
2  
3 @Override
4 protectedvoidconfigure() {
5  
6 bind(IShape.class).to(MyRectangle.class);
7  
8 }
9 }

此时,如果使用injector.getInstance(IShape.class) 或是injector 碰到依赖于IShape地方时,它将使用MyRectangle。可以将类型映射到它任意子类或是实现了该类型接口的所有类。也可以将一个实类(非接口)映射到其子类,如

bind(MyRectangle.class).to(MySquare.class);

下面例子使用@Inject 应用IShape.

1 publicclassLinkedBindingsDemo extendsGraphics2DActivity{
2  
3 @InjectIShape shape;
4  
5 protectedvoiddrawImage(){
6  
7 /**
8 * The semi-opaque blue color in
9 * the ARGB space (alpha is 0x78)
10 */
11 Color blueColor = newColor(0x780000ff,true);
12 /**
13 * The semi-opaque yellow color in the
14 * ARGB space ( alpha is 0x78)
15 */
16 Color yellowColor = newColor(0x78ffff00,true);
17  
18 /**
19 * The dash array
20 */
21 intdashArray[] = { 20,8};
22 graphics2D.clear(Color.WHITE);
23 graphics2D.Reset();
24 Pen pen=newPen(yellowColor,10,Pen.CAP_BUTT,
25 Pen.JOIN_MITER,dashArray,0);
26 SolidBrush brush=newSolidBrush(blueColor);
27 graphics2D.setPenAndBrush(pen,brush);
28 graphics2D.fill(null,shape);
29 graphics2D.draw(null,shape);
30  
31 }
32  
33 }

使用bind(IShape.class).to(MyRectangle.class),为了简化问题,这里定义了MyRectangle和MySquare都带有一个不带参数的构造函数,注入具有带参数的构造函数类用法在后面有介绍。

1 publicclassMyRectangle extendsRectangle{
2 publicMyRectangle(){
3 super(50,50,100,120);
4 }
5  
6 publicMyRectangle(intwidth,intheight){
7 super(50,50,width,height);
8 }
9 }
10 ...
11 publicclassMySquare extendsMyRectangle {
12  
13 publicMySquare(){
14 super(100,100);
15 }
16  
17 publicMySquare(intwidth){
18 super(width,width);
19 }
20  
21 }

Linked bindings 允许链接,例如

1 publicclassGraphics2DModule extendsAbstractAndroidModule{
2  
3 @Override
4 protectedvoidconfigure() {
5 bind(IShape.class).to(MyRectangle.class);
6 bind(MyRectangle.class).to(MySquare.class);
7  
8 }
9 }

此时当需要IShape 时,Injector返回MySquare 的实例, IShape->MyRectangle->MySquare

本例下载

Android RoboGuice 使用指南(5):Binding Annotations

有些情况需要将同一类型映射到不同的类实现,还是使用绘图的例子.

IShape, Rectangle, MyRectangle, MySquare,有如下继承关系:

我们可能需要将IShape 同时映射到MyRectangle 和MySquare ,这时可以使用Binding Annotation 来实现。 这时使用类型和annotation (标注)可以唯一确定一个Binding。Type 和annotation 对称为Key(键)。

为了同时使用MyRectangle和MySequare,我们定义两个annotation,如下

1 importcom.google.inject.BindingAnnotation;
2 importjava.lang.annotation.Target;
3 importjava.lang.annotation.Retention;
4 importstaticjava.lang.annotation.RetentionPolicy.RUNTIME;
5 importstaticjava.lang.annotation.ElementType.PARAMETER;
6 importstaticjava.lang.annotation.ElementType.FIELD;
7 importstaticjava.lang.annotation.ElementType.METHOD;
8  
9 ...
10 @BindingAnnotation
11 @Target({ FIELD, PARAMETER, METHOD })
12 @Retention(RUNTIME)
13 public@interfaceRectangle {
14 }
15 ...
16  
17 @BindingAnnotation
18 @Target({ FIELD, PARAMETER, METHOD })
19 @Retention(RUNTIME)
20 public@interfaceSquare {
21 }

定义了两个标注 @Rectangle, @Square, 至于@BindingAnnotation,@Target,@Retention你并不需要详细了解,有兴趣的可以参见Java Annotation tutorial .

简单的说明如下:

  • @BindingAnnotation 通知这是一个Binding Annotation,如果将多个个标注应用到同一个元素时,Guice会报错。
  • @Target({FIELD, PARAMETER, METHOD}) 表示这个标注可以应用到类成员变量,函数的参数或时方法。
  • @Retention(RUNTIME) 表示这个标注在程序运行时可以使用Reflection读取。

创建一个BindingAnnotationsDemo 用来绘制两个图形:

1 publicclassBindingAnnotationsDemo extendsGraphics2DActivity{
2  
3 @Inject@RectangleIShape shape1;
4 @Inject@SquareIShape shape2;
5  
6 protectedvoiddrawImage(){
7  
8 /**
9 * The semi-opaque blue color in
10 * the ARGB space (alpha is 0x78)
11 */
12 Color blueColor = newColor(0x780000ff,true);
13 /**
14 * The semi-opaque green color in the ARGB space (alpha is 0x78)
15 */
16 Color greenColor = newColor(0x7800ff00,true);
17  
18 graphics2D.clear(Color.WHITE);
19 graphics2D.Reset();
20  
21 SolidBrush brush=newSolidBrush(blueColor);
22  
23 graphics2D.fill(brush,shape1);
24 AffineTransform at = newAffineTransform();
25 at.translate(20,20);
26 graphics2D.setAffineTransform(at);
27 brush=newSolidBrush(greenColor);
28 graphics2D.fill(brush,shape2);
29  
30 }
31  
32 }

使用标注将shape1 绑定到MyRectangle, shape2绑定到MySquare,对应的Module 定义如下:

1 publicclassGraphics2DModule extendsAbstractAndroidModule{
2  
3 @Override
4 protectedvoidconfigure() {
5  
6 bind(IShape.class)
7 .annotatedWith(Rectangle.class)
8 .to(MyRectangle.class);
9  
10 bind(IShape.class)
11 .annotatedWith(Square.class)
12 .to(MySquare.class);
13  
14 }
15 }

Inject 可以应用到Field (成员变量),Parameter (参数)或Method(方法),前面的例子都是应用到Field上,如果应用到参数可以有如下形式:

1 @Inject
2 publicIShape getShape(@RectangleIShape shape){
3 ...
4 }

如果你不想自定义Annotation,可以使用Guice自带的@Name标注来解决同一类型绑定到不同实现的问题。

修改上面代码:

1 //@Inject @Rectangle IShape shape1;
2 //@Inject @Square IShape shape2;
3  
4 @Inject@Named("Rectangle") IShape shape1;
5 @Inject@Named("Square") IShape shape2;

修改绑定如下:

1 //bind(IShape.class)
2 //.annotatedWith(Rectangle.class)
3 //.to(MyRectangle.class);
4  
5 //bind(IShape.class)
6 //.annotatedWith(Square.class)
7 //.to(MySquare.class);
8  
9 bind(IShape.class)
10 .annotatedWith(Names.named("Rectangle"))
11 .to(MyRectangle.class);
12 bind(IShape.class)
13 .annotatedWith(Names.named("Square"))
14 .to(MySquare.class);

这种方法简单,但编译器无法检测字符串,比如将”Square”错写为”Sqare”,编译器无法查出这个错误,此时到运行时才可能发现 shape2 无法注入,因此建议尽量少用Named.

本例下载

Android RoboGuice 使用指南(6):Instance Bindings

我们在前面例子Android RoboGuice 使用指南(4):Linked Bindings 时为简单起见,定义MyRectangle和MySquare时为它们定义了一个不带参数的构造函数,如MyRectangle的如下:

1 publicclassMyRectangle extendsRectangle{
2 publicMyRectangle(){
3 super(50,50,100,120);
4 }
5 publicMyRectangle(intwidth,intheight){
6 super(50,50,width,height);
7 }
8 }

实际上可以不需要这个不带参数的构造函数,可以使用Instance Bindings ,Instance Bindings可以将一个类型绑定到一个特定的实例对象,通常用于一个本身不依赖其它类的类型,如各种基本类型,比如:

1 bind(String.class)
2 .annotatedWith(Names.named("JDBC URL"))
3 .toInstance("jdbc:mysql://localhost/pizza");
4 bind(Integer.class)
5 .annotatedWith(Names.named("login timeout seconds"))
6 .toInstance(10);

修改MyRectangle和MySquare的定义如下:

1 publicclassMySquare extendsMyRectangle {
2 @Inject
3 publicMySquare(@Named("width")intwidth){
4 super(width,width);
5 }
6 }
7 ...
8 publicclassMyRectangle extendsRectangle{
9  
10 @Inject
11 publicMyRectangle(@Named("width")intwidth,
12 @Named("height")intheight){
13 super(50,50,width,height);
14 }
15 }

去掉了无参数的构造函数,可以将标注为@Named(“width”)的int 类型绑定到100,添加下面绑定:

1 bind(Integer.class)
2 .annotatedWith(Names.named("width"))
3 .toInstance(100);
4 bind(Integer.class)
5 .annotatedWith(Names.named("height"))
6 .toInstance(120);

运行这个例子,可以得到和前面例子同样的结果。此时使用Injector 构造一个MyRectangle 实例时,Injector自动选用带参数的那个构造函数,使用100,120为width和height注入参数,返回一个MyRectangle对象到需要引用的地方。

尽管可以使用Instance Bindings将一个类型映射到一个复杂类型的类实例,但RoboGuice不建议将Instance Bindings应用到复杂类型的实例,因为这样会使应用程序启动变慢。

正确的方法是使用@Provides 方法,将在下面介绍。

注:GuiceDemo 中的例子没用使用列表的方法来显示所有示例,如需运行所需示例,可以通过Run Configuration->设置Launch 的Activity:

Android RoboGuice 使用指南(7):@Provides Methods

上例说过如果需要构造一些较复杂的类的实例,通常的方法是使用@Provides 方法。这个方法必须定义在模块中(Module),而且必须使用@Provides 标注,在个方法的返回类型则绑定到这个方法返回的对象实例。

如果这个方法带有binding Annotation或是@Named(“xxx”),Guice则将@Provides方法返回的对象绑定到这个annotated类型。

本例使用@Provides创建三个圆,然后再屏幕上显示出来,图形库的使用可以参见Android简明开发教程十二:引路蜂二维图形库简介及颜色示例其实创建圆并不复杂,这里只是用来说明@Provides方法的用法。

在Graphics2DModule 在添加三个@Provides方法:

1 @Provides@Named("Circle1")
2 IShape provideCircle1(){
3 returnnewEllipse(30,60,80,80);
4 }
5  
6 @Provides@Named("Circle2")
7 IShape provideCircle2(){
8 returnnewEllipse(60,30,80,80);
9 }
10  
11 @Provides@Named("Circle3")
12 IShape provideCircle3(){
13 returnnewEllipse(90,60,80,80);
14 }

分别绑定到IShape带有标注@Named(“Circle1″),@Named(“Circle2″),@Named(“Circle3″).

创建ProvidesMethodsDemo,有如下代码

1 publicclassProvidesMethodsDemoextendsGraphics2DActivity{
2  
3 @Inject@Named("Circle1") IShape circle1;
4 @Inject@Named("Circle2") IShape circle2;
5 @Inject@Named("Circle3") IShape circle3;
6  
7 protectedvoiddrawImage(){
8  
9 // The solid (full opaque) red color in the ARGB space
10 Color redColor = newColor(0xffff0000);
11  
12 // The semi-opaque green color in the ARGB space (alpha is 0x78)
13 Color greenColor = newColor(0x7800ff00,true);
14  
15 // The semi-opaque blue color in the ARGB space (alpha is 0x78)
16 Color blueColor = newColor(0x780000ff,true);
17  
18 // The semi-opaque yellow color in the ARGB space ( alpha is 0x78)
19 Color yellowColor = newColor(0x78ffff00,true);
20  
21 // The dash array
22 intdashArray[] = { 20,8};
23 graphics2D.clear(Color.WHITE);
24 graphics2D.Reset();
25 SolidBrush brush= newSolidBrush(redColor);
26 graphics2D.fill(brush,circle1);
27 brush=newSolidBrush(greenColor);
28 graphics2D.fill(brush,circle2);
29 Pen pen= newPen(yellowColor,10,Pen.CAP_BUTT,Pen.JOIN_MITER,dashArray,0);
30 brush=newSolidBrush(blueColor);
31 graphics2D.setPenAndBrush(pen,brush);
32 graphics2D.fill(null,circle3);
33 graphics2D.draw(null,circle3);
34  
35 }
36  
37 }

@Provides方法通常用来创建将复杂的类对象,可以带参数,参数也可以通过注入传入比如:

1 @Provides@Named("Circle1")
2 IShape provideCircle1( @Named("width")intwidth){
3 returnnewEllipse(30,60,width,width);
4 }

本例下载


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值