Android屏幕适配

一、Android支持的多种屏幕

传统意义上,一般是是这么认为的:

ldpi: 对应分辨率240×320

mdpi: 对应分辨率320×480

hdpi:对应分辨率480×800或480×854

但实际上没有这么简单,直接看官方资料的下标,可以看到其实ldpi一样由480×800,甚至还有1024×600

二、如何分辨是ldpi、mdpi、hdpi?

为什么要分辨率ldpi、mdpi、hdpi?我的理解,是为了要在不同的屏幕密度下取得最好的显示效果。

从上一段来看,通过分辨率来看并不是很靠谱,那怎么样才靠谱?其实,只要我们知道屏幕分辨率、屏幕尺寸(对角线长度),就可以算出相应的屏幕密度,从而根据其范围得出属于那种屏幕密度。

我们可以根据长或者根据宽来计算出dpi,计算公式为:

dpi=宽/((尺寸^2 * 宽^2)/(宽^2 + 高^2))^(1/2)

= 长/((尺寸^2 * 长^2)/(宽^2 + 高^2))^(1/2)

此计算公式可以在excel中予以计算。

大概计算方法如下,以宽为例:

1.比如分辨率为320×480,则长宽比为1:1.5

2.比如屏幕尺寸为3.6”,则根据勾股定理,”长^2+宽^2=3.6^2″,即”宽^2+2.25*宽^2=12.96″,得出”宽^2=12.96/3.25″,则”宽=(12.96/3.25)^(1/2)= 1.9969″

3.宽为320px,分布在1.9969”上,因此密度为320/1.9969=160.2467

4.因此此密度为mdpi的密度

三、粗略的分辨ldpi 、mdpi、hdpi

套用老资料,其实传统意义上的通过分辨率判断手机dpi,还是比较靠谱的:

ldpi: 对应分辨率240×320

mdpi: 对应分辨率320×480

hdpi:对应分辨率480×800或480×854

为什么呢?因为ldpi如果要是320×480,则需要4.8寸的屏幕,如果是480×800,则需要7.8寸的屏幕,如果mdpi是480×800,则需要5.2寸的屏幕,一般的手机屏幕不会这么大,所以还算靠谱。

当然,如果是分辨android pad的dpi,建议还是算一下吧。

四、关于Android 屏幕适配的方法在大致有两种,总结如下:

(1) 针对不同dpi的屏幕各自适配一套资源,这种方法是基于粗略的dpi来适配不同分辨率的屏幕,

(2) 先用一套资源适配好一种屏幕,然后给不同分辨率的屏幕乘以其相应的逻辑密度,即可以适应不同分辨率的屏幕。逻辑密度的计算如下:

float desity=this.getApplicationContext().getResources().getDisplayMetrics().desity

关于Android读取图片资源的总结

有时我们需要读取不同位置的图片资源,比如:drawable,asset,SDCard,以下对其进行了总结。

方式一:

已将图片保存到drawable目录下,通过图片id获得Drawable或者Bitmap,此方式最常用。(若只知道图片的名称,还可以通过图片的名称获得图片的id)

(1)通过图片id获得Drawable

Drawable drawable=getResource().getDrawable(R.drawable.xxx);

(2)通过图片id获得Bitmap

Resource res=gerResource();

Bitmap bitmap=BitmapFactory.decodeResource(res, id);

(3)通过图片的名称获得图片的id(两种方法)

方法1:   int id =res.getIdentifier(name, defType, defPackage); //name:图片的名,defType:资源类型(drawable,string…),defPackage:工程的包名

方法2:

// 用反射机制来获取资源中的图片ID和尺

Field[] fields = R.drawable.class.getDeclaredFields()

for (Field field : fields) {
    if (!”icon”.equals(field.getName())) {
        // 除了icon之外的图片
        int index = 0;
        try {
            index = field.getInt(R.drawable.class);
        } catch (IllegalArgumentException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        // 保存图片ID
        imgList.add(index);
    }
}

获得id之后可以根据你的需要来获得Bitmap或Drawable

方式二:

已将图片保存到assest目录下,知道图片的名称,通过inputstream获得图片Drawabl

或者 Bitmap

AssetManager asm=getAssetMg();

InputStream is=asm.open(name);//name:图片的名称

//获得Drawable
Drawable da = Drawable.createFromStream(is, null);

//获得Bitmap
Bitmap bitmap=BitmapFactory.decodeStream(is);

方式三:图片保存在sdcard,通过图片的路径h

/图片路径
String imgFilePath = Environment.getExternalStorageDirectory().toString()
+ “/DCIM/device.png”;

(1)文件输入流

public Bitmap getBitmapFromSd(String  imgFilePath) {

    FileInputStream fis = null;

    try {
        fis = new FileInputStream(new File(imgFilePath));//文件输入流
        Bitmap bmp = BitmapFactory.decodeStream(fis);
        Log.d(“sss”, bmp + “”);
        return bmp;
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } finally{
        try {
            if(fis!=null){
                fis.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

(2)随机读写文件流

RandomAccessFile raf;

File imgfile = new File(path);

try {
    raf = new RandomAccessFile(imgfile, “r”);//以读的方式读取文件流
} catch (IOException ex) {
    e.printStackTrace();
} finally{
    try {
        if(fis!=null){
            fis.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

data= new byte[1024];
try {
    mMiniThumbFile.seek(0);
    int got = mMiniThumbFile.read(data, 0, 1024);
} catch (IOException e) {
    e.printStackTrace();
}
if (data != null) {
    //通过data获得bitmap
    Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
}

建议使用随机读写文件流,可以防止读取的文件流过大而导致内存溢出

 

android内存溢出解决

1,解决使用Bitmap时出现的内存溢出

1)及时的销毁,虽然,系统能够确认Bitmap分配的内存最终会被销毁,但是由于它占用的内存过多,所以很可能会超过java堆的限制。因此,在用完Bitmap时,要及时的recycle掉。recycle并不能确定立即就会将Bitmap释放掉,但是会给虚拟机一个暗示:“该图片可以释放了”。

2)设置一定的采样率,有时候,我们要显示的区域很小,没有必要将整个图片都加载出来,而只需要记载一个缩小过的图片,这时候可以设置一定的采样率,那么就可以大大减小占用的内存。如下面的代码:

private ImageView preview;  
BitmapFactory.Options options = new BitmapFactory.Options();  
options.inSampleSize = 2; 
Bitmap bitmap = BitmapFactory.decodeStream(cr.openInputStream(uri), 
null, options);  
preview.setImageBitmap(bitmap);

3)运用软引用(SoftRefrence),有些时候,我们使用Bitmap后没有保留对它的引用,因此就无法调用Recycle函数。这时候巧妙的运用软引用,可以使Bitmap在内存快不足时得到有效的释放。如下例:

private class MyAdapter extends BaseAdapter {  

     private ArrayList mBitmapRefs = new ArrayList();  
     private ArrayList mValues;  
     private Context mContext;  
     private LayoutInflater mInflater;  

     MyAdapter(Context context, ArrayList values) {  
         mContext = context;  
         mValues = values;  
         mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);  
     }  
     public int getCount() {  
         return mValues.size();  
     }  

     public Object getItem(int i) {  
         return mValues.get(i);  
     }  

     public long getItemId(int i) {  
         return i;  
     }  

     public View getView(int i, View view, ViewGroup viewGroup) {  
         View newView = null;  
         if(view != null) {  
             newView = view;  
         } else {  
             newView =(View)mInflater.inflate(R.layout.image_view, false);  
         }  

         Bitmap bitmap = BitmapFactory.decodeFile(mValues.get(i).fileName);  
         mBitmapRefs.add(new SoftReference(bitmap));     //此处加入ArrayList  
         ((ImageView)newView).setImageBitmap(bitmap);  

         return newView;  
     }  
}

2,解决Context引起的内存溢出:
在Android平台上,长期保持一些资源的引用,造成一些内存不能释放,带来的内存泄露问题很多。比如Context。
android中的很多资源文件都需要一个Context引用来加载,如果这些资源没有被释放,那么Context的引用不为null,造成对应的Activity即使 调用了finish()但其占有的内存依然不能被释放。这是 因为在Java或者Android内存机制中,顶点的结点释放前必须保证其他对象没有调用才能被系统GC回收释放。我们来看一段代码:

SoundManager.getInstance(this).play(SoundManger.MAIN_BG_SOUND);

从这段代码可以看出,声音管理类是一个单例,它对于整个应用进程来说是全局的,在进入应用的时候创建这个单利直到应用结束,这个单例才会被释放。大家可以看到这个类在得到单例的时候需要传递一个Context对象作为参数,因为要利用Context来加载声音资源。这就导致如果当前Activity调用了finish()全依然不能被GC,因为声音管理类是全局的,它持有了当前Activity的应用,阻止了其被GC。 解决的办法是尽量使用全局的Context来加载资源。修改如下:

SoundManager.getInstance(this.getApplicationContext).play(SoundManger.MAIN_BG_SOUND);

3,解决Thread 线程引起的内存溢出

如下代码:

private class MyThread extends Thread{

@Override

public void run() {

super.run();
while(bFlag)
{
//do somthing
}
}

MyThread mThread = new MyThread();

mThread.start();

这端代码 在主线程里新开了一个线程,并且在线程里循环处理一些逻辑。问题在于 如果控制线程结束的bFlag如果在Activity销毁时没有置为 false 将会产生很严重的后果。线程的一个特点是生命周期的不可控。如果Activity销毁时,没有结束线程的运行,那么不仅阻止了Activity被GC,而且大大降低了程序的性能。假如,再重新进入这个Activity,那么又创建了一个死循环的线程,而之前的那个线程依然在运行,这样程序就会非常的卡。所以 一定要注意,销毁Activity时一定要结束线程。

总而言之,想要避免context 相关的内存泄漏 ,记住以下几点:
a.不要对activity 的context 长期引用( 一个activity 的引用的生存周期应该和activity 的生命周期相同)
b.试着使用关于application的 context 来替代和activity相关的context
c.如果一个acitivity 的非静态内部类的生命周期不受控制,那么避免使用它;使用一个静态的内部类并且对其中的activity 使用一个弱引用。解决这个问题的方法是使用一个静态的内部类,并且对它的外部类有一WeakReference,就像在ViewRoot中内部类W所做的就是这么个例子。

Bitmap拉伸后出现锯齿的几种解决办法

图片拉伸后出现严重锯齿的情况,总结了下,有如下几种方案

1.给Paint加上抗锯齿标志。然后将Paint对象作为参数传给canvas的绘制方法。

paint.setAntiAlias(true);

canvas.drawBitmap(bmp ,fX,fY,paint);

2.直接给canvas加抗锯齿。

canvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG|Paint.FILTER_BITMAP_FLAG));

这两种方法  大多数情况下都能解决问题,但是部分情况下效果不太理想。除此之外还可以尝试下面两种方法:

3.  创建bitmap时就做拉伸处理:

Bitmap bmp = Bitmap.createScaledBitmap(bmpSrc, (int) (bmpSrc.getWidth() * scale), (int) (bmpSrc.getHeight() * scale), true);

注意这个 方法的最后一个参数一定要设为true,否则无法去锯齿

4 .在绘制图像时利用matrix对图像进行拉伸。

Matrix matrix = new Matrix();

matrix.setTranslate(fX,fY);

matrix.postScale(scale,scale,fX,fY);

canvas.drawBitmap(bmp,matrix,null);

Android 编程下的代码混淆

什么是代码混淆

Java 是一种跨平台的、解释型语言,Java 源代码编译成中间”字节码”存储于 class 文件中。由于跨平台的需要,Java 字节码中包括了很多源代码信息,如变量名、方法名,并且通过这些名称来访问变量和方法,这些符号带有许多语义信息,很容易被反编译成 Java 源代码。为了防止这种现象,我们可以使用 Java 混淆器对 Java 字节码进行混淆。

混淆就是对发布出去的程序进行重新组织和处理,使得处理后的代码与处理前代码完成相同的功能,而混淆后的代码很难被反编译,即使反编译成功也很难得出程序的真正语义。被混淆过的程序代码,仍然遵照原来的档案格式和指令集,执行结果也与混淆前一样,只是混淆器将代码中的所有变量、函数、类的名称变为简短的英文字母代号,在缺乏相应的函数名和程序注释的况下,即使被反编译,也将难以阅读。同时混淆是不可逆的,在混淆的过程中一些不影响正常运行的信息将永久丢失,这些信息的丢失使程序变得更加难以理解。

混淆器的作用不仅仅是保护代码,它也有精简编译后程序大小的作用。由于以上介绍的缩短变量和函数名以及丢失部分信息的原因, 编译后 jar 文件体积大约能减少25% ,这对当前费用较贵的无线网络传输是有一定意义的。

混淆文件 proguard.cfg 参数详解

-optimizationpasses 5                                                           # 指定代码的压缩级别
-dontusemixedcaseclassnames                                                     # 是否使用大小写混合
-dontskipnonpubliclibraryclasses                                                # 是否混淆第三方jar
-dontpreverify                                                                  # 混淆时是否做预校验
-verbose                                                                        # 混淆时是否记录日志
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*        # 混淆时所采用的算法

-keep public class * extends android.app.Activity                               # 保持哪些类不被混淆
-keep public class * extends android.app.Application                            # 保持哪些类不被混淆
-keep public class * extends android.app.Service                                # 保持哪些类不被混淆
-keep public class * extends android.content.BroadcastReceiver                  # 保持哪些类不被混淆
-keep public class * extends android.content.ContentProvider                    # 保持哪些类不被混淆
-keep public class * extends android.app.backup.BackupAgentHelper               # 保持哪些类不被混淆
-keep public class * extends android.preference.Preference                      # 保持哪些类不被混淆
-keep public class com.android.vending.licensing.ILicensingService              # 保持哪些类不被混淆

-keepclasseswithmembernames class * {                                           # 保持 native 方法不被混淆
    native <methods>;
}

-keepclasseswithmembers class * {                                               # 保持自定义控件类不被混淆
    public <init>(android.content.Context, android.util.AttributeSet);
}

-keepclasseswithmembers class * {
    public <init>(android.content.Context, android.util.AttributeSet, int);     # 保持自定义控件类不被混淆
}

-keepclassmembers class * extends android.app.Activity {                        # 保持自定义控件类不被混淆
   public void *(android.view.View);
}

-keepclassmembers enum * {                                                      # 保持枚举 enum 类不被混淆
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

-keep class * implements android.os.Parcelable {                                # 保持 Parcelable 不被混淆
  public static final android.os.Parcelable$Creator *;
}

-keep class MyClass;                                                            # 保持自己定义的类不被混淆

代码混淆的方法 根据 SDK 的版本不同有 2 中不同的代码混淆方式,以上的 proguard.cfg 参数详解中所涉及到的信息是在较低版本 SDK 下的混淆脚本,事实上在高版本的 SDK 下混淆的原理和参数也与低版本的相差无几,只是在不同 SDK 版本的环境下引入混淆脚本的方式有所不同。具体方法如下:

    • 低版本 SDK 下,项目中同时包含 proguard.cfg 和 project.properties 文件,则只需在 project.properties 文件末尾添加 proguard.config=proguard.cfg 再将项目 Export即可。
  • 高版本 SDK 下,项目中同时包含 proguard-project.txt 和 project.properties 文件,这时需要在 proguard-project.txt 文件中进行如下信息的配置,然后再将项目Export 即可。下面以真实的文件进行演示说明。
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system edit
# "ant.properties", and override values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt

proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt

# Project target.
target=android-16

以上的配置信息即是 project.properties 文件中内容,蓝色文字为我们在代码混淆过程中需要添加的配置信息,其中:sdk.dir 为你在当前机器上 SDK 的安装路径。如果想保留某个包下的文件不被混淆,可以在 proguard-project.txt 文件中加入保留对应包名的语句即可。

# To enable ProGuard in your project, edit project.properties
# to define the proguard.config property as described in that file.
#
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ${sdk.dir}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the ProGuard
# include property in project.properties.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# Add any project specific keep options here:

-dontwarn com.cnki.android.cnkireader.** 
-keep class com.cnki.android.cnkireader.** { *; }

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}

Android四大组件之服务(Service)

服务(Service) 是一种在后台运行,没有界面的组件,由其他组件调用开始。Android 中的服务和 Windows 中的服务是类似的东西,它运行于系统中不容易被用户发觉,可以使用它开发如监控之类的程序。

服务(Service)的实现

1. 定义类继承 Service,重写 onCreate()、 onStart(Intent intent, int startId)、 onBind(Intent intent)、 onUnbind(Intent intent)、onDestroy() 中需要的方法。

2. 在清单文件的 <application> 节点下声明 <service>。

<application
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme" >
    <activity
        android:name=".MainActivity"
        android:label="@string/title_activity_main" >
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>

    <service android:name=".MyService" >
    </service>
</application>

服务不能自己运行,需要通过调 用 Context.startService() 或 Context.bindService() 方法启动服务。这两个方法都可以启动 Service,但是它们的使用场合有所不同。使用 startService() 方法启用服务,访问者与服务之间没有关联,即使访问者退出了,服务仍然运行。使用 bindService() 方法启用服务,访问者与服务绑定在了一起,访问者一旦退出,服务也就终止。采用 Context.startService() 方法启动服务,只能调用 Context.stopService() 方法结束服务,服务结束时会调用 onDestroy() 方法。【注意:服务的生命周期方法都是在主线程执行,所以不能直接在服务里面做耗时操作做,如果需要做耗时操作,应该在服务里面开启子线程。】

通过 startService() 和 stopService( ) 启动和关闭服务。适用于服务和访问者之间没有交互的情况。如果服务和访问者之间需要方法调用或者传递参数,则需要使用 bindService() 和 unbindService() 方法绑定和解绑服务。

采用 Context.bindService() 方法启动服务,在服务未被创建时,系统会先调用服务的 onCreate() 方法,接着调用 onBind() 方法,这个时候访问者和服务绑定在一起。 如果访问者要与服务进行通信,那么,onBind() 方法必须返回 Ibinder 对象。如果访问者退出了,系统就会先调用服务的 onUnbind() 方法,接着调用 onDestroy() 方法。如果调用 bindService() 方法前服务已经被绑定,多次调用 bindService() 方法并不会导致多次创建服务及绑定(也就是 说 onCreate() 和 onBind() 方法并不会被多次调用)。如果访问者希望与正在绑定的服务解除绑定,可以调用 unbindService() 方法,调用该方法也会导致系统调用服务的 onUnbind() → onDestroy() 方法。

服务(Service)的生命周期(服务的生命周期与启动服务的方法有关)

  • 当采用 Context.startService() 方法启动服务,生命周期如下:

onCreate() → onStart() → onDestroy()

onCreate() 该方法在服务被创建时调用,该方法只会被调用一次,无论调用多少次 startService() 或 bindService() 方法,服务也只被创建一次。onStart() 只有采用 Context.startService() 方法启动服务时才会回调该方法,该方法在服务开始运行时被调用。多次调用 startService() 方法尽管不会多次创建服务,但 onStart() 方法会被多次调用。onDestroy() 该方法在服务被终止时调用。

  • 当采用 Context.bindService() 方法绑定服务,生命周期如下:

onCreate() → onBind() → onUnbind() → onDestroy()

onBind() 只有采用 Context.bindService() 方法启动服务时才会回调该方法。该方法在调用者与服务绑定时被调用,当调用者与服务已经绑定,多次调用 Context.bindService() 方法并不会导致该方法被多次调用。onUnbind() 只有采用 Context.bindService() 方法绑定服务后解绑服务时才会回调该方法。该方法在调用者与服务解除绑定时被调用。

  • 如果先采用 startService() 方法启动服务,然后调用 bindService() 方法绑定到服务,再调用 unbindService() 方法解除绑定,最后调用 bindService() 方法再次绑定到服务,生命周期如下:

onCreate() → onStart() → onBind() → onUnbind()[重载后的方法需返回true] → onRebind()

android小试之答题王

这几天利用空余的时间学习了下android,选择了类似答题的应用来边做边学,美名曰:答题王

既然刚开始,也不搞那么难,能做一个简单的就ok了

首先我定义了三个页面:

1) 首页:FirstPageActivity

2) 答题页面:AnswerQuestionActivity

3) 结果页面:ResultActivity

具体流程是:

1) 用户在首页选择试题类型(有3套题目)

2) 根据用户选择的题型在数据库中查找对应的题目,用户答题

3) 显示用户答题的成绩,并获取用户的排名

主要知识点:

1)android简单布局,元素使用及监听

2)android数据存储及查询(sqlite使用)

3)页面之间数据传递

4)访问服务端接口获取数据

1. 首页

使用3个button,每个button监听用户是否点击

Activity的主要代码是:

Button button1;
Button button2;
Button button3;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.firstpage);
    button1 = (Button)this.findViewById(R.id.Button01);
    button2 = (Button)this.findViewById(R.id.Button02);
    button3 = (Button)this.findViewById(R.id.Button03);
    button1.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            startNewActivity(1);
        }
    });

    button2.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            startNewActivity(2);
        }
    });

    button3.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            startNewActivity(3);
        }
    });
}

private void startNewActivity(int parm) {
    Intent i = new Intent();
    i.setClass(FirstPageActivity.this, AnswerQuestionActivity.class);
    i.putExtra("exam_id", parm);
    startActivity(i);
    FirstPageActivity.this.finish();
}

以上使用Intent类作为页面之间的跳转,并且可以在页面之间传递参数。

2. 答题页面

1) 首先需要通过首页传过来的参数查询数据库,数据库使用的是sqlite,里面有一张表t_question,在查询数据库之前首先要将数据库放入应用程序的数据目录下:切换到DDMS视图下,选择File Explorer标签,进入/data/data/(package)/databases,在右上角可以看到有一个代右箭头的图标,点击这个图标,选择需要导入的数据库文件。

查询数据库的代码:

private List> loadData() {
    Intent intent = this.getIntent();
    int exam_id = intent.getIntExtra("exam_id", 0);
    mSQLiteDatabase = this.openOrCreateDatabase("dati_db.s3db", MODE_PRIVATE, null);
    String sql = "select * from t_questions where exam_id="+exam_id;
    Cursor cursor = mSQLiteDatabase.rawQuery(sql, null);
    List> datas = new ArrayList>();
    if(cursor != null) {
        Log.v("loadData", "cursor is not null");
        HashMap map = null;
        if(cursor.moveToFirst()) {
            do{
                try {
                    map = new HashMap();
                    int questionColumnIdx = cursor.getColumnIndex("question");
                    byte[] question = cursor.getBlob(questionColumnIdx);
                    map.put("question", new String(question,"GBK"));
                    int answersColumnIdx = cursor.getColumnIndex("answer");
                    byte[] answer = cursor.getBlob(answersColumnIdx);
                    map.put("answer", new String(answer,"GBK"));
                    int correct_answer = cursor.getInt(cursor.getColumnIndex("correct_answer"));
                    map.put("correct_answer", String.valueOf(correct_answer));
                    datas.add(map);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }while(cursor.moveToNext());
        }
    }
    return datas;
}

以上使用了SQLiteDatabase类(mSQLiteDatabase)操作数据库,将查询的数据封装成一个List对象,值得注意的是,因为数据库中有中文,我使用了cursor.getBlob()方法获取数据后再转化编码的方式。

答题页面的样式为一个文本标题,另外还有答案选项,答案选项我使用的是单项选择类RadioGroup和RadioButton,所以有一个方法:resetQuestion(设置问题)

private void resetQuestion(List> datas, int question_index) {
    if(question_index < datas.size()) {
         question_text.setText(datas.get(question_index).get("question"));
         String answers = datas.get(question_index).get("answer");
         String[] ansArr = answers.split("\\|");
         ans_radioGroup.removeAllViews();
         for(String s : ansArr) {
             RadioButton radioButton = new RadioButton(this.getApplicationContext());
             radioButton.setText(s);
             radioButton.setTextColor(Color.BLACK);
             ans_radioGroup.addView(radioButton);
         }
         correct_answer = Integer.parseInt(datas.get(question_index).get("correct_answer"));
    }else {
        String deviceId = tm.getDeviceId();
        Log.v("deviceId", deviceId);
        String rankStr = RequestUtil.sendGet("http://XXX.XX.XX.XX/rank.php", "user="+deviceId+"&total="+datas.size()+"&correct="+correct_number);

        Log.v("rankResult", rankStr);
        try {
            JSONObject jsonObj = new JSONObject(rankStr);
            int rank = Integer.parseInt(jsonObj.get("rank").toString());
            Log.v("rank", String.valueOf(rank));
            this.goIntoResultView(rank);
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }
}

question_index为定义的一个全局变量,当用户回答的问题数小于总数,则执行初始化题目的页面,这里使用一个ans_radioGroup变量就可以了,监听用户的行为,代码后面会讲到,每次都需要使用ans_radioGroup.removeAllViews()清除掉上次的答案选项,再根据下次答案选项初始化单选按钮的个数,correct_answer也是一个全局变量,记录当前题目的正确选项。

当用户答玩所有的题目后获取用户的排名,跳转到成绩页面,这里通过获取手机的设备号来标识唯一用户,使用:

final TelephonyManager tm = (TelephonyManager) getBaseContext().getSystemService(Context.TELEPHONY_SERVICE);
String deviceId = tm.getDeviceId();

另外向服务端请求数据,我封装了一个类RequestUtil,发送一个GET请求,具体代码如下:

/**
 * 向指定URL发送GET方法的请求
 * 
 * @param url
 *        发送请求的URL
 * @param params
 *        请求参数,请求参数应该是name1=value1&name2=value2的形式。
 * @return URL所代表远程资源的响应
 */
public static String sendGet(String url, String params) {
    String result = "";
    BufferedReader in = null;
    try {
        String urlName = url + "?" + params;
        URL realUrl = new URL(urlName);
        // 打开和URL之间的连接
        URLConnection conn = realUrl.openConnection();
        // 设置通用的请求属性
        conn.setRequestProperty("accept", "*/*");
        conn.setRequestProperty("connection", "Keep-Alive");
        conn.setRequestProperty("user-agent",
                "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)");
        in = new BufferedReader(
                new InputStreamReader(conn.getInputStream()));
        String line;
        while ((line = in.readLine()) != null) {
            result += "\n" + line;
        }
    } catch (Exception e) {
        System.out.println("发送GET请求出现异常!" + e);
        e.printStackTrace();
    }
    finally {
        try {
            if (in != null) {
                in.close();
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
    return result.length()>1?result.substring(1):result;
}

4)ans_radioGroup监听用户的行为:

ans_radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
    Log.v("correct_answer", String.valueOf(correct_answer));
    RadioButton correctButton = getRadioButton(correct_answer); 
    if(checkedId == correctButton.getId()) {
        displayToast("回答正确");
        correct_number ++;
    }else {
        displayToast("回答错误");
    }
    question_index ++;
    resetQuestion(datas, question_index);
}
});

里面有两个小方法:

4.1) getRadioButton(correct_answer),根据正确答案,获取对应的RadioButton,correct_answer我在数据库中存储的是题目的编号,具体方法代码如下:

private RadioButton getRadioButton(int correct_answer) {
    return (RadioButton)ans_radioGroup.getChildAt(correct_answer - 1);
}

因为RadioGroup是从0开始计数的

4.2)  displayToast(str),提示框,具体介绍可查看这里,代码如下:

private Toast displayToast(String str) {
    Toast toast = Toast.makeText(this, str, Toast.LENGTH_SHORT);
    toast.setGravity(Gravity.TOP, 0, 220);
    toast.show();
    return toast;
}

 3. 成绩页面

很简单,获取答题页面传过来的参数,显示即可。