图解Fiddler如何抓手机APP数据包【超详细】
1、PC端安装Fiddler
下载地址:Fiddler.exe,http://www.telerik.com/download/fiddler
2、 配置PC端Fiddler和手机
(1) 配置Fiddler允许监听https
打开Fiddler菜单项Tools->Fiddler Options,选中decrypt https traffic和ignore server certificate errors两项,如下图:
第一次会提示是否信任fiddler证书及安全提醒,选择yes,之后也可以在系统的证书管理中进行管理。
(2) 配置Fiddler允许远程连接
如上图的菜单中点击connections,选中allow remote computers to connect,默认监听端口为8888,若被占用也可以设置,配置好后需要重启Fiddler,如下图:
(3) 配置手机端
Pc端命令行ipconfig查看Fiddler所在机器ip,本机ip为10.0.4.37,如下图
打开手机连接到同一局域网的wifi,并修改该wifi网络详情(长按wifi选择->修改网络)->显示高级选项,选择手动代理设置,主机名填写Fiddler所在机器ip,端口填写Fiddler端口,默认8888,如下图:
这时,手机上的网络访问在Fiddler就可以查看了,如下图微博和微信的网络请求:
可以双击上图某一行网络请求,右侧会显示具体请求内容(Request Header)和返回内容(Response Header and Content),如下图:
可以发现Fiddler可以以各种格式查看网络请求返回的数据,包括Header, TextView(文字), ImageView(图片), HexView(十六进制),WebView(网页形式), Auth(Proxy-Authenticate Header), Caching(Header cache), Cookies, Raw(原数据格式), JSON(json格式), XML(xml格式)很是方便。
停止网络监控的话去掉wifi的代理设置即可,否则Fiddler退出后手机就上不网了哦。
如果需要恢复手机无密码状态,Android端之后可以通过系统设置-安全-受信任的凭据-用户,点击证书进行删除或清除凭据删除所有用户证书,再设置密码为无。
如果只需要监控一个软件,可结合系统流量监控,关闭其他应用网络访问的权限。
利用fiddler抓取Android app数据包
做Android开发的朋友经常需要做网络数据的获取和提交表单数据等操作,然而对于调试程序而言,很难知道我们的数据到底是以怎样的形式发送的,是否发送成功,如果发送失败有是什么原因引起的。fiddler工具为我们提供了很方便的抓包操作,可以轻松抓取浏览器的发出的数据,不管是手机APP,还是web浏览器,都是可以的。
fiddler的工作原理
fiddler是基于代理来实现抓取网络数据包的工作的,当我们开启fiddler以后,fiddler会将我们的浏览器的代理默认进行更改为 127.0.0.1 端口是8888,这时fiddler的默认端口,也就是说我们发送的每一个请求和收到的每一个响应都会先经过fiddler,这样就实现了抓取数据包的工作。
路径:选项?>高级设置?>更改代理服务器设置?>局域网设置?>高级
9.回话面板说明:
session会话的分析
这里我随便选择一个会话来进行简单的分析。
替换服务器端返回的数据
利用”autoresponser”可以替换服务器端返回的文件,当调试的时候需要替换服务器端返回的数据的时候,比如一个已经上线的项目,不可能真正的替换器某一个文件,我们可以这样来操作
从图片当中,可以很清晰的看出,当我再次加载该会话的时候,会显示之前设置好的404代理。
如果需要设置不同的文件代理,也是可以的。比如对于该会话,原本服务器端返回的内容如下图:
由于该session返回的是一个图片类型的,所以我选择ImageView这个选项卡,可以看到此时返回的图片的样子,那么如果需要用本地的文件代理该返回的内容,和之前的操作步骤都是一样的,只是在选择代理的时候选择本地文件即可,如下图:
这次,我选择了一个本地的文件作为代理,此时当我再次重新请求该会话的时候,会返回本地的文件:
可以看出这个时候该会话返回的内容已经是我本地的代理了。
fiddler网络限速
fiddler还为我们提供了一个很方便的网络限速的功能,通过网络限速的功能,可以来模拟用户的一些真实环境。fiddler提供了网络限速的插件,我们可以在他的官网下载:http://www.telerik.com/fiddler/add-ons
点击”download”,下载完成之后,点击安装,需要重新启动fiddler,在重新启动fiddler之后,可以看到fiddler的工具栏选项卡,多出了一个FiddlerScript选项。
比如我需要在请求之前延迟一段时间,可以这样做:
在onBeforeRequest方法中加入这样一段代码”oSession[“request-trickle-delay”] = “3000”;”,那么如果需要在服务端响应之间做延迟只需要将”oSession[“request-trickle-delay”] = “3000”;”中的request替换成response即可。
利用fiddler抓取Android app数据包
终于到了今天的主题了,如何利用fiddler抓取Android app数据包,其实也是很简单的,只需要稍微配置一下就可以了。由于fiddler默认是抓取http协议的数据包,我们需要其能够抓取https这样的加密数据包,抓取Android app数据包,需要做如下配置:
1.配置fiddler
点击工具栏选项”tools?>FiddlerOptions”
配置https:
配置远程连接:
这些配置完成之后,一定要重新启动fiddler。
可以看到fiddler的默认端口是8888,我们可以现在浏览器上输入”http://127.0.0.1:8888”
到这里为止我们的fiddler就配置完成了,接下来需要配置手机上的无线网络。
2.手机无线网络配置
注意:如果需要fiddler抓取Android app上的数据包,那么两者必须在同一个无线网络中。(同时,必要时请关闭电脑的防火墙)
在手机的无线网络配置之前,必须要首先知道fiddler所在主机的ip地址:
可以看到我的fiddler所在主机,也就是我的电脑在无线网中的ip地址是192.168.1.109
打开手机设置中的无线网络界面,进行如下四步操作:
选中连接的网络,点击修改网络
点击高级选项
代理—>手动
输入代理服务器的ip,也就是我们fiddler所在主机的ip地址,和端口,fiddler默认的端口是8888,IP选项设置为”DHCP”
点击保存,此时手机端就配置成功了,打开fiddler,使用打开网易新闻客户端。
此时可以看到fiddler抓取的网易app发送和接收的相关数据包。
ok,左侧是我们的所有会话,我随机的选中一个会话,该会话是image类型的,查看该会话的内容,是我们网易新闻的头条上的图片。
注意:
1.关闭电脑的防火墙
2.如果需要抓取手机app的数据包,需要手机和电脑在都连接同一个无线网络
3.抓完包以后将fiddler关闭(提高访问网络的速度)同时将手机上的代理关闭 (如果不关闭代理,当fiddler关闭,或者是两者连接的不是同一无线网络,手机会不能正常的访问网络)
APP开发出现异常在所难名,甚至会导致应用程序崩溃。如果在debug模式下开发的时候,是可以通过查看logcat日志来查看异常消息,从而进行处理。但是,如果我们在发布版本之后,用户在使用的时候crash掉了,就无法查看异常信息,也就很难找出bug来解决问题。
还好在java线程类中,有一个针对上述问题的解决办法:在线程中捕捉未处理的异常。因为crash时,抛出的异常就是因为没有在app中catch处理,就会抛给系统,如果我们在这个时候对这个能够对这个异常进行处理,就最好不过了,这样就能打印异常信息,就能发布给服务器,供开发人员查看。
一,写一个CrashHanler类
package com.raise.wind.utils; import android.os.Environment; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; /** * Created by yu on 2015/7/15. */ public class CrashHandler implements Thread.UncaughtExceptionHandler { public static CrashHandler instance; private CrashHandler() { } public static CrashHandler get_instance() { if (instance == null) new CrashHandler(); return instance; } public void init() { Thread.setDefaultUncaughtExceptionHandler(this); } @Override public void uncaughtException(Thread thread, Throwable ex) { saveFile(ex.getMessage(), "crash.txt"); //退出程序 //这里由于是我们自己处理的异常,必须手动退出程序,不然系统出一只处于crash等待状态 android.os.Process.killProcess(android.os.Process.myPid()); System.exit(1); } public static void saveFile(String data, String file_name) { File sdPath = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "aacrash" + File.separator + "cache"); if (!sdPath.exists()) { sdPath.mkdirs(); } File file = new File(sdPath, file_name); FileOutputStream fos = null; try { fos = new FileOutputStream(file); fos.write(data.getBytes("UTF-8")); } catch (Exception e) { e.printStackTrace(); } finally { if (fos != null) try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } } }
二,在Application中声明
记得在Androidanifest.xml中配置
package com.raise.wind.app; import android.app.Application; import com.raise.wind.utils.CrashHandler; /** * Created by yu on 2015/7/15. */ public class APP extends Application { @Override public void onCreate() { super.onCreate(); CrashHandler.get_instance().init(); } }
三,测试
这样就声明了我们线程中出现的为捕捉异常交给CrashHandler类来处理。
现在我们写一个空指针异常来测试:
package com.raise.wind.crashproject; import android.os.Bundle; import android.support.v7.app.ActionBarActivity; import android.widget.TextView; public class MainActivity extends ActionBarActivity { TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView.setText("text"); } }
打开app,发现程序立即crash,打开文件管理能找到在程序中保存的文件,里面有异常消息
Unable to start activity ComponentInfo{com.raise.wind.crashproject/com.raise.wind.crashproject.MainActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.TextView.setText(java.lang.CharSequence)' on a null object reference
当然,这只是异常消息的开头的提示,如果要想全部打印出来,使用下面代码:
Could not execute method of the activity android.view.View$1.onClick(View.java:4010) android.view.View.performClick(View.java:4759) android.view.View$PerformClick.run(View.java:19770) android.os.Handler.handleCallback(Handler.java:739) android.os.Handler.dispatchMessage(Handler.java:95) android.os.Looper.loop(Looper.java:135) android.app.ActivityThread.main(ActivityThread.java:5235) java.lang.reflect.Method.invoke(Native Method) java.lang.reflect.Method.invoke(Method.java:372) com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:906) com.android.internal.os.ZygoteInit.main(ZygoteInit.java:701) Caused by null java.lang.reflect.Method.invoke(Native Method) java.lang.reflect.Method.invoke(Method.java:372) android.view.View$1.onClick(View.java:4005) android.view.View.performClick(View.java:4759) android.view.View$PerformClick.run(View.java:19770) android.os.Handler.handleCallback(Handler.java:739) android.os.Handler.dispatchMessage(Handler.java:95) android.os.Looper.loop(Looper.java:135) android.app.ActivityThread.main(ActivityThread.java:5235) java.lang.reflect.Method.invoke(Native Method) java.lang.reflect.Method.invoke(Method.java:372) com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:906) com.android.internal.os.ZygoteInit.main(ZygoteInit.java:701) Caused by Attempt to invoke virtual method 'void android.widget.TextView.setText(java.lang.CharSequence)' on a null object reference com.raise.wind.crashproject.MainActivity.click_1(MainActivity.java:21) java.lang.reflect.Method.invoke(Native Method) java.lang.reflect.Method.invoke(Method.java:372) android.view.View$1.onClick(View.java:4005) android.view.View.performClick(View.java:4759) android.view.View$PerformClick.run(View.java:19770) android.os.Handler.handleCallback(Handler.java:739) android.os.Handler.dispatchMessage(Handler.java:95) android.os.Looper.loop(Looper.java:135) android.app.ActivityThread.main(ActivityThread.java:5235) java.lang.reflect.Method.invoke(Native Method) java.lang.reflect.Method.invoke(Method.java:372) com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:906) com.android.internal.os.ZygoteInit.main(ZygoteInit.java:701)
这个类一般结合log4j来记录日志,并且在发生crash时,将log文件发送到服务器。
这样程序就可以查看用户手机端的crash消息了,方便我们处理在debug模式开发时未发现的异常。
Android APP级异常捕获实现方式
描述:App级异常捕获,并记录下CrashLog到文件。
以下,代码。
在Application的,onCreate中,初始化自定义的CrashHandler
import android.app.Application; import com.tjd.appexceptioncatch.exception.CrashHandler; public class MyApplication extends Application { private static MyApplication instance; @Override public void onCreate() { super.onCreate(); CrashHandler.getInstance().init(getApplicationContext()); } public static MyApplication getInstance() { if (instance == null) { instance = new MyApplication(); } return instance; } }
自定义CrashHandler如下
import java.io.File; import java.io.FileOutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.io.Writer; import java.lang.Thread.UncaughtExceptionHandler; import java.lang.reflect.Field; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Map; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Build; import android.os.Environment; import android.os.Looper; import android.util.Log; import android.widget.Toast; import com.tjd.appexceptioncatch.application.MyApplication; /** * UncaughtException处理类,当程序发生Uncaught异常的时候,有该类来接管程序,并记录发送错误报告. * 需要在Application中注册,为了要在程序启动器就监控整个程序。 */ public class CrashHandler implements UncaughtExceptionHandler { public static final String TAG = "CrashHandler"; //系统默认的UncaughtException处理类 private Thread.UncaughtExceptionHandler mDefaultHandler; //CrashHandler实例 private static CrashHandler instance; //程序的Context对象 private Context mContext; //用来存储设备信息和异常信息 private Map<String, String> infos = new HashMap<String, String>(); //用于格式化日期,作为日志文件名的一部分 private DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss"); /** 保证只有一个CrashHandler实例 */ private CrashHandler() { } /** 获取CrashHandler实例 ,单例模式 */ public static CrashHandler getInstance() { if (instance == null) instance = new CrashHandler(); return instance; } /** * 初始化 */ public void init(Context context) { mContext = context; //获取系统默认的UncaughtException处理器 mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler(); //设置该CrashHandler为程序的默认处理器 Thread.setDefaultUncaughtExceptionHandler(this); } /** * 当UncaughtException发生时会转入该函数来处理 */ @Override public void uncaughtException(Thread thread, Throwable ex) { if (!handleException(ex) && mDefaultHandler != null) { //如果用户没有处理则让系统默认的异常处理器来处理 mDefaultHandler.uncaughtException(thread, ex); } else { try { Thread.sleep(3000); } catch (InterruptedException e) { Log.e(TAG, "error : ", e); } //退出程序 android.os.Process.killProcess(android.os.Process.myPid()); System.exit(1); } } /** * 自定义错误处理,收集错误信息 发送错误报告等操作均在此完成. * @param ex * @return true:如果处理了该异常信息;否则返回false. */ private boolean handleException(Throwable ex) { if (ex == null) { return false; } //收集设备参数信息 collectDeviceInfo(mContext); //使用Toast来显示异常信息 new Thread() { @Override public void run() { Looper.prepare(); Toast.makeText(mContext, "很抱歉,程序出现异常,即将退出.", Toast.LENGTH_SHORT).show(); Looper.loop(); } }.start(); //保存日志文件 saveCatchInfo2File(ex); return true; } /** * 收集设备参数信息 * @param ctx */ public void collectDeviceInfo(Context ctx) { try { PackageManager pm = ctx.getPackageManager(); PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_ACTIVITIES); if (pi != null) { String versionName = pi.versionName == null ? "null" : pi.versionName; String versionCode = pi.versionCode + ""; infos.put("versionName", versionName); infos.put("versionCode", versionCode); } } catch (NameNotFoundException e) { Log.e(TAG, "an error occured when collect package info", e); } Field[] fields = Build.class.getDeclaredFields(); for (Field field : fields) { try { field.setAccessible(true); infos.put(field.getName(), field.get(null).toString()); Log.d(TAG, field.getName() + " : " + field.get(null)); } catch (Exception e) { Log.e(TAG, "an error occured when collect crash info", e); } } } private String getFilePath() { String file_dir = ""; // SD卡是否存在 boolean isSDCardExist = Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); // Environment.getExternalStorageDirectory()相当于File file=new File("/sdcard") boolean isRootDirExist = Environment.getExternalStorageDirectory().exists(); if (isSDCardExist && isRootDirExist) { file_dir = Environment.getExternalStorageDirectory().getAbsolutePath() + "/crashlog/"; } else { // MyApplication.getInstance().getFilesDir()返回的路劲为/data/data/PACKAGE_NAME/files,其中的包就是我们建立的主Activity所在的包 file_dir = MyApplication.getInstance().getFilesDir().getAbsolutePath() + "/crashlog/"; } return file_dir; } /** * 保存错误信息到文件中 * @param ex * @return 返回文件名称,便于将文件传送到服务器 */ private String saveCatchInfo2File(Throwable ex) { StringBuffer sb = new StringBuffer(); for (Map.Entry<String, String> entry : infos.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); sb.append(key + "=" + value + "\n"); } Writer writer = new StringWriter(); PrintWriter printWriter = new PrintWriter(writer); ex.printStackTrace(printWriter); Throwable cause = ex.getCause(); while (cause != null) { cause.printStackTrace(printWriter); cause = cause.getCause(); } printWriter.close(); String result = writer.toString(); sb.append(result); try { long timestamp = System.currentTimeMillis(); String time = formatter.format(new Date()); String fileName = "crash-" + time + "-" + timestamp + ".log"; String file_dir = getFilePath(); // if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { File dir = new File(file_dir); if (!dir.exists()) { dir.mkdirs(); } File file = new File(file_dir + fileName); if (!file.exists()) { file.createNewFile(); } FileOutputStream fos = new FileOutputStream(file); fos.write(sb.toString().getBytes()); //发送给开发人员 sendCrashLog2PM(file_dir + fileName); fos.close(); // } return fileName; } catch (Exception e) { Log.e(TAG, "an error occured while writing file...", e); } return null; } /** * 将捕获的导致崩溃的错误信息发送给开发人员 * 目前只将log日志保存在sdcard 和输出到LogCat中,并未发送给后台。 */ private void sendCrashLog2PM(String fileName) { // if (!new File(fileName).exists()) { // Toast.makeText(mContext, "日志文件不存在!", Toast.LENGTH_SHORT).show(); // return; // } // FileInputStream fis = null; // BufferedReader reader = null; // String s = null; // try { // fis = new FileInputStream(fileName); // reader = new BufferedReader(new InputStreamReader(fis, "GBK")); // while (true) { // s = reader.readLine(); // if (s == null) // break; // //由于目前尚未确定以何种方式发送,所以先打出log日志。 // Log.i("info", s.toString()); // } // } catch (FileNotFoundException e) { // e.printStackTrace(); // } catch (IOException e) { // e.printStackTrace(); // } finally { // 关闭流 // try { // reader.close(); // fis.close(); // } catch (IOException e) { // e.printStackTrace(); // } // } } }
在MainActivity中触发异常
import android.app.Activity; import android.os.Bundle; public class MainActivity extends Activity { final String str = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); str.equals("exception"); } }
Activity是Android组件中最基本也是最为常见用的四大组件(Activity,Service服务,Content Provider内容提供者,BroadcastReceiver广播接收器)之一。它间接继承自android.content.Context,因此,有些时候都直接把Activity实例当做Context的实例来使用。
如前面所提到的要在应用程序中使用Activity,必须在Android Manifest.xml中配置它。
新建一个Android工程,新建过程中勾选create activity,让系统自动帮我们创建一个Activity并在Android Manifest.xml中配置它。
AndroidManifest.xml代码如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="cn.csc.activity" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="14" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name=".FirstActivity" 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> </manifest>
配置中,Activity节点比较重要的属性有:
android:name属性:指明该Activity节点对应的Activity定义所在的类名,这里默认简写为.FisrtActivity,完整的类名需要与Manifest节点的package属性进行拼接,也可以直接写完整的类名,这里即为cn.csc.activity.FirstActivity。注意,name属性是必须配置的,否则报错,毕竟不配置name属性本身就没有任何意义了。
android:label属性:指明该Activity的标题栏显示的内容。
此外,比较重要的还有一个:
android:launchMode属性:指明该Activity的加载模式。取值可以是standard、singleTop、singleTask和singleInstance。这个属性在提到Activity生命周期时会用到。
回到FirstActivity.java的代码来看:
public class FirstActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.first_layout); } }
Activity中常用的方法:
void onCreate(Bundle savedInstanceState) :这个方法在该Activity被创建时回调,进行相关的初始化工作。如,我们最常做的setContentView();设置一个UI界面。
void setContentView(int layoutResID)
void setContentView(View view) :这两个方法用于给Activity设置UI界面,只是传入的参数类型不同,一个是传入一个layout资源id,一个是直接在代码中编写UI界面。
View findViewById(int id) :根据控件的id找到该id,返回值是一个View实例,通常需要进行向下转型到具体类型,如Button、TextView等,以便对控件进行操作,如设置控件属性值,进行事件绑定等。
在first_layout.xml中添加一个按钮,需要设置按钮的id属性,在FirstActivity中的onCreate()方法中根据id获取该按钮,然后设置它的单击响应,弹出一个Toast信息。
代码如下:
first_layout.xml:
<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" > <Button android:id="@+id/btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/btnText" /> </RelativeLayout>
FirstActivity.java:
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.first_layout); Button btn = (Button) findViewById(R.id.btn); btn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub Toast.makeText(FirstActivity.this, "I'm clicked", Toast.LENGTH_SHORT).show(); } }); }
void startActivity(Intent intent) :用于启动一个新的Activity,参数intent中指定了要启动Activity的相关信息。
void finish() :用于结束,并销毁当前Activity。
新建一个Activity,名为SecondActivity;新建一个layout文件,名为second_layout.xml
修改first_layout中按钮的点击事件,使它启动SecondActivity。
代码如下:
second_layout.xml:
<?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="match_parent" android:orientation="vertical" > <Button android:id="@+id/btn2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/close"/> </LinearLayout>
SecondActivity.java:
protected void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onCreate(savedInstanceState); setContentView(R.layout.second_layout); Button btn2 = (Button) findViewById(R.id.btn2); btn2.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub finish(); } }); }
FirstActivity.java:
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.first_layout); Button btn = (Button) findViewById(R.id.btn); btn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub // Toast.makeText(FirstActivity.this, "I'm clicked", Toast.LENGTH_SHORT).show(); Intent intent = new Intent(FirstActivity.this, SecondActivity.class); startActivity(intent); } }); }
注意:使用SecondActivity一定要在Manifest.xml中配置
<activity android:name=".SecondActivity" android:label="@string/second">
</activity>
否则会出现如下错误:
点击FirstActivity中的按钮,会启动SecondActivity,点击SecondActivity中的按钮会销毁SecondActivity,然后又回到FirstActivity中。跟点击模拟器上的返回键效果一样。
点击I am a button
点击Close或者返回键:
关于Intent的使用,将在之后详细说明。
void startActivityForResult(Intent intent, int requestCode) :用于启动一个新的Activity,并期望在这个新的Activity结束时返回数据。
void onActivityResult(int requestCode, int resultCode, Intent data) :用于接收处理启动的新的Activity结束时返回的数据。
这两个函数在Intent在Android之间传递数据时会用到。
Intent getIntent() :获取启动该Activity的意图实例,该方法可以实现获取该Activity的启动者所要传递给自己的存放在Intent中的数据。
关于管理Activity的任务栈:
Activity中有一个int getTaskId()方法 :用于获取当前Activity所处的栈的id。
修改FirstActivity中的onCreate():
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.first_layout); Button btn = (Button) findViewById(R.id.btn); Log.i("TaskId","First:"+getTaskId()); btn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub // Toast.makeText(FirstActivity.this, "I'm clicked", Toast.LENGTH_SHORT).show(); Intent intent = new Intent(FirstActivity.this, SecondActivity.class); startActivity(intent); } }); }
及SecondActivity中的onCreate():
protected void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onCreate(savedInstanceState); setContentView(R.layout.second_layout); Button btn2 = (Button) findViewById(R.id.btn2); Log.i("TaskId","Second:"+getTaskId()); btn2.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub finish(); } }); }
注意:Log.i("TaskId","First:"+getTaskId());用于在LogCat中输出程序运行信息,第一个为Tag参数,第二个要输出的字符串信息。
打开LogCat界面:window ----> show view ----> other ---->即可找到LogCat。
由于显示的运行信息比较多,可以添加一个信息过滤器,只显示我们所关心的信息。
点击绿色的加号
由于我们在之前的Log.i()中设定了Tag参数为TaskId,这时选择by Log Tag,然后填入我们所设置的TaskId即可,随便给过滤器取个名字,然后OK。
启动应用程序,若发现仍有很多运行信息:
此时,单击下TaskId会发现只剩下一条了
此时运行的是FirstActivity,显示的是FirstActivity所在的任务栈的id为15。然后点击程序中的按钮,启动SecondActivity,发现又多出一条信息:
发现SecondActivity所在的任务栈id同样为15。
一个android应用中,不可能只有一个Activity,如上面启动了两个Activity。Android中使用任务栈来管理多个Activity。
当一个Activity被创建启动时,就会把它放入一个任务栈的栈顶。当该Activity结束被销毁时,就会从所在任务栈弹出,其下的Activity变为栈顶,切换到活动状态。当有新的Activity被启动时,它会入栈称为新的栈顶,之前活动的Activity被强制切换到暂停或者停止状态。
任意时刻,只有栈顶的Activity处于活动状态,可以与用户进行交互。栈中其他Activity若是仍然可见或部分可见,即没有被当前活动Activity完全遮盖时,则处于暂停状态。若完全不可见,则处于停止状态。
一般来说,一个android应用中所有的Activity都会被放到同一个任务栈中进行统一管理,但是也有例外,如上面提到的在Manifest.xml中配置launchMode时,配置不同的值就会有所差别。
关于Activity的状态:
任意时刻,一个Activity都处于下面四个状态之一:
运行状态:位于任务栈的栈顶,此时能够与用户进行交互。
暂停状态:不再处于任务栈的栈顶,不能与用户交互,但是仍然有部分可见。
停止状态:不再处于任务栈的栈顶,不能与用户交互,而且完全不可见。
销毁状态:已从任务栈中弹出。
当系统内存不足时,会优先回收处于销毁状态的Activity所占用的资源;仍然不足时,会回收处于停止状态的Activity所占用的资源;仍然不足时,会回收处于暂停状态的Activity的资源;最不愿意回收的是运行状态的Activity资源。
关于Activity的加载模式与任务栈的关联:
前面提到android:launchMode属性:指明该Activity的启动模式。取值可以是standard、singleTop、singleTask和singleInstance。
standard模式:是默认的启动模式,若没有明确指定启动模式,则为standard模式。如上面的FirstActivity和SecondActivity都没有设置launchMode属性,即为standard模式。
在该模式下,每当新启动一个Activity时,都会为之创建一个新的实例,然后放入栈顶,而不在乎任务栈中是否已然存在该Activity的实例。
修改FirstActivity代码:
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.first_layout); Button btn = (Button) findViewById(R.id.btn); Log.i("TaskId","First:"+this+" "+getTaskId()); btn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub // Toast.makeText(FirstActivity.this, "I'm clicked", Toast.LENGTH_SHORT).show(); Intent intent = new Intent(FirstActivity.this, FirstActivity.class); startActivity(intent); } }); }
Log.i("TaskId","First:"+this+" "+getTaskId());此处,加上this,打印出当前实例引用this的值。
按钮的点击响应改为启动FirstActivity自身。
运行信息:
发现每次this的值都不同,可见每次都新建了一个FirstActivity实例,即便当前FirstActivity实例已然位于任务栈中,且位于任务栈的栈顶。
singleTop模式:standard模式很多时候明显不太合理,当前Activity已然有实例位于任务栈的栈顶,直接使用不就得了,干嘛非要再创建一个实例浪费资源呢。singleTop模式就是当要启动的Activity已然有实例位于任务栈的栈顶就直接使用当前栈顶,而不重新创建实例。
修改Manifest.xml:
<activity android:name=".FirstActivity" android:label="@string/first" android:launchMode="singleTop" >
修改FirstActivity代码:
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.first_layout); Button btn = (Button) findViewById(R.id.btn); Log.i("TaskId","Create:"+this+" "+getTaskId()); btn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(FirstActivity.this, FirstActivity.class); Log.i("TaskId","Click:"+FirstActivity.this+" "+getTaskId()); startActivity(intent); } }); }
Log.i("TaskId","Create:"+this+" "+getTaskId());表示是onCreate()中调用
在button的onclick中Log.i("TaskId","Click:"+FirstActivity.this+" "+getTaskId());表明是点击按钮调用。
运行信息:
发现只有一个Create,即onCreate()方法只调用了一次,只创建了一个FirstActivity实例。
若FirstActivity实例在任务栈中,但是不是在栈顶,又会如何呢?
修改代码,FirstActivity中启动SecondActivity,而SecondActivity又启动FirstActivity,两者均配置为singleTop。
修改FirstActivity代码:
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.first_layout); Button btn = (Button) findViewById(R.id.btn); Log.i("TaskId","Create First:"+this+" "+getTaskId()); btn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(FirstActivity.this, SecondActivity.class); startActivity(intent); } }); }
修改SecondActivity代码:
protected void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onCreate(savedInstanceState); setContentView(R.layout.second_layout); Button btn2 = (Button) findViewById(R.id.btn2); Log.i("TaskId","Create Second:"+this + " "+getTaskId()); btn2.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub Intent intent = new Intent(SecondActivity.this, FirstActivity.class); startActivity(intent); } }); }
运行信息:
发现每次都新建了一个实例,即使当前Activity在任务栈中已然存在,但由于并没与处于栈顶,就要再次创建新的实例。
singleTask模式:可能会觉得singleTop模式还是浪费资源,明明栈中已然存在实例,只因为它不在栈顶便要重新创建。若是想重用不在栈顶的Activity实例,则需要使用singleTask模式,该模式下,新启动一个Activity时,检查当前任务栈中是否存在实例,若存在,但是不在栈顶也没关系,系统会把所有在这个实例之上的Activity实例统统出栈,这样这个实例就处于栈顶,然后就可以直接重用了。
修改Manifest.xml将两个Activity的launchMode都改为singleTask,源代码不需要改动,此时观察运行信息:
启动FirstActivity时,输出一条信息,点击FirstActivity中的按钮,由于SecondActivity在栈中不存在,创建了一个实例,然后点击SecondActivity中的按钮,发现FirstActivity实例已然存在,但是栈顶是SecondActiviy,则会将SecondActivity实例出栈,直接重用已存在的FirstActivity实例,所以,此时没有回调onCreate(),故而没有输出信息。
singleInstance模式:用于共享Activity时使用。设置为该模式的Activity不会与当前应用中的其他Activity公用一个任务栈,而是出于自己单独的任务栈中。而几个公用这个Actiivty的应用,共享这个单独的任务栈,就实现了该Activity实例的共享。不然的话,每个应用都有自己的任务栈,启动的Activity肯定在自己的任务栈中管理,根本做不到Activity实例的共享。
修改程序,添加一个ThirdActivity,程序运行的效果FirstActivity为入口,在其中点击按钮可以启动SecondActivity,SecondActivity的launchMode设置为singleInstance,点击SecondActivity中的按钮,可以启动ThirdActivity。ThirdActivity中的按钮可以启动SecondActivity。
FirstActivity和ThirdActivity的launchMode均设置为singleTask。
运行信息:
FirstActivity和ThirdActivity都在同一个栈中,id为28
而SecondActivity在id为29的栈中。
注意,此时,点击ThirdActivity中的按钮启动SecondActivity时,不会输出任何信息,因为直接重用了id为29的任务栈中的SecondActivity实例。若在ThirdActivity时,按下模拟器的返回按钮,销毁ThirdActivity,则会直接回到FirstActivity中,因为它俩是处在同一个栈中的。
关于Activity的生命周期:
下面是Activity整个生命周期中,状态发生变化时所回调的方法,它们对应着Activity完整的生命过程。
void onCreate(Bundle savedInstanceState):Activity被创建时回调
void onStart() :在onCreate()或者onRestart()之后被调用,即Activity第一次创建或者从不可见变为可见状态时调用。
void onResume() :恢复到活动状态时回到,在onStart()之后一定会调用该方法。之后该活动就处于活动状态了,处于任务栈的栈顶。
void onPause() :失去焦点,但是仍然部分可见时回调。
void onStop() :Activity变为完全不可见时回调
void onRestart() :Activity重新启动时回调
void onDestroy() :Activity被销毁前回调
上面的7个方法,除了onRestart()之外,在生命周期的图中都是成对出现的。分为三对,也就出现了三种生存期。
从onCreate()到onDestroy(),一个Activity实例经历了创建到销毁的所有过程,被称之为完整生存期。
从onStart()到onStop(),一个Activity实例从可见状态变为不可见状态,被称之为可见生存期。注意,可见并不一定处于栈顶,因而并一定能与用户交互。
从onResume()到onPause(),一个Activity实例经历了从活动状态到暂停状态,这两个方法之间的过程,该Activity实例都处于活动状态,被称之为前台生存期,或者活动状态生存期。
完整生命周期程序演示,参考《第一行代码》
程序有三个Activity:MainActivity是入口,放置两个按钮,分别用于启动另外两个Activity,实现7个生命周期回调方法,分别输出一条运行信息;NormalActivity就是一个普通的Activity;DialogActivity在Manifest.xml中配置了theme属性,使其成为一个对话框样式的Activity,<activity android:name=".DialogActivity" android:theme="@android:style/Theme.Dialog"></activity>。
具体代码:
main_layout.xml:
<?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="match_parent" android:orientation="vertical" > <Button android:id="@+id/normal" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/start_normal_activity"/> <Button android:id="@+id/dialog" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/start_dialog_activity"/> </LinearLayout>
MainActivity.java:
public class MainActivity extends ActionBarActivity implements OnClickListener { @Override public void onClick(View view) { // TODO Auto-generated method stub switch (view.getId()) { case R.id.normal: Intent intent1 = new Intent(this, NormalActivity.class); startActivity(intent1); break; case R.id.dialog: Intent intent2 = new Intent(this, DialogActivity.class); startActivity(intent2); default: break; } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_layout); Log.i("LIFECYCLE","onCreate"); Button btnNormal = (Button) findViewById(R.id.normal); Button btnDialog = (Button) findViewById(R.id.dialog); btnNormal.setOnClickListener(this); btnDialog.setOnClickListener(this); } @Override protected void onStop() { // TODO Auto-generated method stub super.onStop(); Log.i("LIFECYCLE","onStop"); } @Override protected void onDestroy() { // TODO Auto-generated method stub super.onDestroy(); Log.i("LIFECYCLE","onDestroy"); } @Override protected void onPause() { // TODO Auto-generated method stub super.onPause(); Log.i("LIFECYCLE","onPause"); } @Override protected void onStart() { // TODO Auto-generated method stub super.onStart(); Log.i("LIFECYCLE","onStart"); } @Override protected void onRestart() { // TODO Auto-generated method stub super.onRestart(); Log.i("LIFECYCLE","onRestart"); } @Override protected void onResume() { // TODO Auto-generated method stub super.onResume(); Log.i("LIFECYCLE","onResume"); } }
运行信息:
首先启动该应用程序,依次输出:
可见,正如生命周期图中所示,依次调用了onCreate()、onStart()、onResume()。
然后点击第一个按钮,启动那个普通的Activity,依次输出:
可见,当NormalActivity启动时,MainActivity调用onPause()进入暂停状态,由于NormalActivity启动后,MainActivity被NormalActivity完全遮住时,又要调用onStop()进入停止状态。
然后,点击模拟器的返回按钮,依次输出:
可见,由于按下返回键后,NormalActivity被销毁,MainActivity由不可见状态变为可见状态,则依次调用onRestart()、onStart(),又由于MainActivity当前处于任务栈栈顶,所以又调用onResume()进入活动状态。
然后,点击第二个按钮,启动DialogActivity,依次输出:
由于MainActivity仍然有部分可见,只是当前不再处于任务栈栈顶而已,所以调用了onPause()进入暂停状态。
然后,按下模拟器上的返回按钮,依次输出:
DialogActivity被销毁,MainActivity重新回到栈顶,调用onResume()进入活动状态。
然后,再按下模拟器上的返回按钮,依次输出:
MainActivity要被销毁,从活动状态到销毁状态,依次调用了onPause()、onStop()和onDestroy()。
以上就是一个完整的Activity生命周期演示。
此外,由于停止状态和暂停状态的Activity有可能被系统回收资源,当一个Activity从暂停或者停止状态重新回到活动状态时,由于可能已经被回收依次,之前的操作、数据等,如填写了好大一张表单,全都要重新开始,用户体验极差。这时,就要用到涉及Activity实例状态保存的回调函数:
onSaveInstanceState(Bundle bundle):用于在被系统回收之前,将需要保存的一些Activity实例状态信息,重要数据等保存到bundle对象中。当该Activity实例下次被创建时,调用onCreate(Bundle bundle)方法时,这个bundle对象会传递给onCreate()方法,则可以在onCreate方法中,获取到上次保存的数据,进行相应的初始化,恢复工作。
Android的UI组件都是继承View类,View表示一个空白的矩形区域。TextView、Button、EditText这些常用的组件等都是直接或间接继承自View。
此外,View还有一个重要的子类ViewGroup,该类可以用来包含多个View组件,本身也可以当做一个View组件被其他的ViewGroup所包含,由此,可以构建出非常复杂的UI界面。
常用的布局管理器如FrameLayout、LinearLayout、RelativeLayout等都直接继承自ViewGroup。
在Android应用中,Activity就相当于传统桌面开发中的Form,刚创建出来就是一个空白的屏幕,因此,要显示UI界面时,就需要调用setContentView()方法传入要显示的视图实例或者布局资源。
如:
传入一个布局资源:
setContentView(R.layout.main);
传入一个View实例:
TextView myTv = new TextView(this);
setContentView(myTv);
myTv.setText(“hello, world”);
因为setContentView()只能接受一个View实例,要显示复杂的UI界面,就需要用到ViewGroup来包含多个多个View实例,然后将ViewGroup实例传给setContentView。ViewGroup是个抽象类,一般直接使用的都是它的子类,被称之为布局管理器。
Android有两种方式编写UI界面,一种是在xml布局资源文件中,另一种是直接在代码中编写,如上面的传入一个View实例的做法就是直接在代码中编写,这是传统的Form编程的做法。现在比较推荐的是在xml布局资源文件中编写UI界面,这样一来就可以将应用表示层与逻辑层相分离,无需修改代码就可以修改表示层。
要编写复杂的UI界面,需要掌握android中常用的布局管理器。主要有:
AbsoluteLayout:绝对布局
FrameLayout:帧布局
LinearLayout:线性布局
RelativeLayout:相对布局
TableLayout:表格布局
GridLayou:网格布局(Android 4.0添加的新的布局管理器)
1.LinearLayout 线性布局
线性布局就是放在其中的View组件将进行线性对齐排列,可以设置是垂直排列还是水平排列。
新建一个布局资源文件的方法:
右击res/layout,然后在弹出的菜单中选择new,然后选择Android Xml File,要新建LinearLayout布局文件,就选择LinearLayout作为其根节点即可。
linear_layout.xml代码如下:
<?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="match_parent" android:orientation="vertical"> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="aaaaaa" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="bbbbbb" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="cccccc" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="dddddd" /> </LinearLayout>
activity中代码如下:
protected void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onCreate(savedInstanceState); setContentView(R.layout.linear_layout); }
显示效果:
常用的几个属性:
1)orientation属性:设置LinearLayout中组件的排列方式,可以取值vertical或者horizontal表示垂直排成一列或者水平排成一行。
上面代码中,如果把orientation设置为horizontal。显示则变为:
因为只显示一行,而第一个Button的宽度就是充满父元素,所以只显示出来了第一个Button。
2)layout_width属性:设置在父元素中该组件的宽度,可以取值wrap_content、match_parent或者fill_parent。其中wrap_content表示宽度能够包裹该组件中的内容即可,fill_parent和match_parent含义相同表示宽度充满父元素,现在,更常使用match_parent,而很少用fill_parent。
如上面代码中把所有的Button的layout_width都设置为wrap_content,则显示效果如下:
3)layout_height属性:设置在父元素中该组件的宽度,取值同layout_width。
4)grativity属性:设置该容器内组件的对齐方式。
如在LinearLayout节点中添加属性:android:gravity="center_vertical"
则显示效果如下:
该属性的取值可以是:top、bottom、left、right、center、center_vertical、center_horizontal等值,或者这些值相或(即位或运算 | )
如:android:gravity="bottom|right" 显示效果
5)layout_gravity属性:当前控件在父元素的位置。
如将aaaaaa那个Button中layout_gravity设置为”center”,其效果将会与其所处容器即LinearLayout中的gravity属性效果进行叠加,显示如下:
垂直上进行了居中,水平上还是排在bbbbbb的左边
6)layout_weight属性:在子控件中设置父元素中多出来的额外空间的分配权重。
此时,如果只在aaaaaa这个button中设置layout_weight属性,可以设置为任意值,习惯设置为1。则aaaaaa这个button会拉伸占据剩下的空间,显示如下:
如果同时在aaaaaa和dddddd两个button中都设置layout_weight属性,且第一个设置为1,第二个设置为2,则之前多出来的剩余空间会分给aaaaaa 1/3,分给dddddd 2/3,即各自的权重值/总的权重值,即为各自所分得的剩余空间的比例,显示如下:
7)weightSum属性:设置容器中剩余空间的总的权重值,这个属性是LinearLayout中的属性,而layout_weight是各个子控件中的属性,若不设置,则默认为各个子控件layout_weight属性值的总和。
若如上面aaaaaa的layout_weight值为1,dddddd的layout_weight的值为2,同时在LinearLayout中设置weightSum值为6,则仍会有一半的剩余空间,aaaaaa只分得原来剩余空间的1/6,dddddd分得2/6,显示如下:
8)visibility属性:控制是否显示,取值可以是invisible、visible、gone。visible表示显示出来,invisible和gone不显示出来,其中invisible不显示,但控件仍然存在,占用着空间,而gone表示控件不存在了,也就不占用空间了。
如:cccccc设置visibility属性为gone,显示如下:
若改为invisible:
LinearLayout设置invisible:
2.RelativeLayout:相对布局
顾名思义,即根据各控件的相对位置进行布局,相对位置,可以是子控件A相对父控件的位置,也可以是子控件A相对于子控件B的位置。
右击res/layout,然后在弹出的菜单中选择new,然后选择Android Xml File,要新建RelativeLayout布局文件,就选择RelativeLayout作为其根节点即可。文件名为relative_layout.xml。
代码如下:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <Button android:id="@+id/aa" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="aaaaaa" /> <Button android:id="@+id/bb" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toRightOf="@id/aa" android:layout_alignTop="@id/aa" android:text="bbbbbb" /> <Button android:id="@+id/cc" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toLeftOf="@id/aa" android:layout_alignBottom="@id/aa" android:text="cccccc" /> <Button android:id="@+id/dd" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_above="@id/aa" android:layout_alignLeft="@id/aa" android:text="dddddd" /> <Button android:id="@+id/ee" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/aa" android:layout_alignLeft="@id/aa" android:text="eeeeee" /> </RelativeLayout>
修改FirstActivity中setContentView(R.layout.relative_layout);
显示效果:
aaaaaa在父容器中居中显示
bbbbbb在aaaaaa的右边显示,并且与aaaaaa顶部对齐
ccccccc在aaaaaa的左边显示,并且与aaaaaa顶部对齐
dddddd在aaaaaa的上面显示,并且与aaaaaa左对齐
eeeeee在aaaaaa的下面显示,并且与aaaaaa左对齐
主要属性:均为设置父子相对位置,或者子控件与子控件的相对位置
android:layout_toRightOf 在指定控件的右边
android:layout_toLeftOf 在指定控件的左边
android:layout_above 在指定控件的上边
android:layout_below 在指定控件的下边
android:layout_alignBaseline 跟指定控件水平对齐
android:layout_alignLeft 跟指定控件左对齐
android:layout_alignRight 跟指定控件右对齐
android:layout_alignTop 跟指定控件顶部对齐
android:layout_alignBottom 跟指定控件底部对齐
android:layout_alignParentLeft 是否跟父布局左对齐
android:layout_alignParentTop 是否跟父布局顶部对齐
android:layout_alignParentRight 是否跟父布局右对齐
android:layout_alignParentBottom 是否跟父布局底部对齐
android:layout_centerVertical 在父布局中垂直居中
android:layout_centerHorizontal 在父布局中水平居中
android:layout_centerInParent 在父布局中居中
3.FrameLayout:帧布局
如同Flash或者photoshop中图层的概念,在上面的图层遮盖下面的图层,没被遮到的地方仍然显示出来。
右击res/layout,然后在弹出的菜单中选择new,然后选择Android Xml File,要新建FrameLayout布局文件,就选择FrameLayout作为其根节点即可。文件名为frame_layout.xml。
代码如下:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <ImageView android:src="@drawable/bg" android:layout_gravity="center" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <ImageView android:src="@drawable/hero" android:layout_width="wrap_content" android:layout_gravity="center" android:layout_height="wrap_content"/> </FrameLayout>
依次放置两个ImageView用于显示两张图片,第一张为背景图片,第二张为一个人物图片。
修改FirstActivity中setContentView(R.layout.frame_layout);
显示效果如下:
先添加的控件位于下面,后添加的控件位于上面。
4.AbsoluteLayout:绝对布局
根据绝对坐标位置进行布局,不灵活,故而很少使用。
eclipse中也提示:AbsoluteLayout is deprecated,即不建议使用绝对布局。
新建一个layout文件,名为absolute_layout.xml,代码入下:
<?xml version="1.0" encoding="utf-8"?> <AbsoluteLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_x="100dp" android:layout_y="100dp" android:text="aaaaaa" /> </AbsoluteLayout>
修改FirstActivity中代码:setContentView(R.layout.absolute_layout);
显示如下:
属性:
android:layout_x 指定控件在父布局的x轴坐标
android:layout_y 指定控件在父布局的y轴坐标
5.TableLayout:表格布局
需要配合TableRow进行使用,也不是太常用。
在TableLayout中每加入一个TableRow子节点,就表示在表格中加入了一行,之后在TableRow中每加入一个控件,就表示加入了一列。注意TableRow单元行里的单元格的宽度小于默认的宽度时就不起作用,其默认是fill_parent,高度可以自定义大小。
如果直接在TableLayout中添加控件,那么该控件将会占用一行。
新建一个layout布局文件,名为table_layout.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?> <TableLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="aaaaaa" /> <TableRow > <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="bbbbbb" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="cccccc" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="bbbbbb" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="cccccc" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="bbbbbb" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="cccccc" /> </TableRow> <TableRow > <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="dddddd" /> </TableRow> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="eeeeee" /> </TableLayout>
修改setContentView(R.layout.table_layout);
显示效果如下:
第0行一个按钮aaaaaa
第1行6个按钮,但是父控件宽度有限,只显示了5个
第2行一个TableRow,里面加了一个按钮dddddd
第3行也是一个按钮eeeeee
主要属性:
android:shrinkColumns 设置收缩的列,其值为要收缩的列的索引,从0开始,多个时用逗号分隔。
如:android:shrinkColumns="0,2",表示第0和第2列收缩,显示效果如下:
可以看出没有放在TableRow中的行没有被收缩。
android:stretchColumns 设置拉伸的列,其值为要收缩的列的索引,从0开始,多个时用逗号分隔。如上显示中第1行有6个按钮,父容器宽度不够用,此时拉伸任何一列都不会有效果。若第1行只有两个按钮,此时,设置android:stretchColumns="1",则会把第1列拉伸,充满父容器剩下的空间。显示效果如下:
android:collapseColumns 设置要隐藏的列,这里的隐藏于visibility设置为gone效果相同的。隐藏之后不占用父容器的空间。
如:android:collapseColumns="1,3,5",则第一行6个按钮,只剩下3个bbbbbb
6.GridLayout:网格布局
Android4.0中新增的布局管理器。因此,在android4.0之后的版本才可以直接使用。
新建项目设置最小SDK为14,其他也要高于14。
新建一个layout文件,名为grid_layout.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?> <GridLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:columnCount="5" android:rowCount="4"> <Button android:layout_row="0" android:text="aaaaaa" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <Button android:layout_row="1" android:layout_column="0" android:text="bbbbbb" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <Button android:layout_row="2" android:layout_column="2" android:text="cccccc" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <Button android:layout_row="3" android:layout_column="1" android:text="dddddd" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </GridLayout>
显示效果如下:
整个布局被分为4行5列。
主要属性:
android:columnCount 设置GridLayout的列数
android:rowCount设置GridLayou的行数
每个添加到GridLayout中的子控件都可以设置如下属性:
android:layout_row 设置该元素所在行,从0开始
android:layout_column 设置该元素所在列,从0开始
android:layout_rowSpan 设置该元素所跨的行数
android:layout_columnSpan 设置该元素所跨的列数。
在手机app应用中我们经常会看到图片轮播动画效果,Android中想要实现图片轮播,主要用到ViewPager这个控件来实现,这个控件的主要功能是实现图片的滑动效果。
那么有了滑动,在滑动的基础上附上图片也就实现了图片轮播的效果...这个控件类似于ListView,需要使用到适配器这个东西,适配器在这里的作用是为轮播时设置一些效果...这里需要使用到PagerAdapter适配器...下面来一个例子,这个例子的效果是在图片轮播的同时显示播放的是第几张图片的信息...并且下面的点也是会随之进行变化的...
先上一下布局文件的代码...这个布局文件其实还是有点说道的...这句话必须要引进...否则会出现错误...意思就是我设置了一个滑动的效果,这个效果填充整个FrameLayout...每一个View表示一个控件,这个控件的显示方式在另外的xml文件当中...下面是两个xml文件...
上面通过配置xml文件来完成View的显示方式,因为这五个点的形状,大小,甚至是显示方式基本都是相同的,如果再去找5个点图片或者是一个点图片,然后通过Drawable资源的调用完成图片的显示...通过加载5次的方式...这样显然是没有必要的,会浪费不必要的资源..因此我们可以使用xml提供的自定义图形来完成这个过程...xml为我们提供了shape属性,自定义控件..这里我定义了一个实心圆...这个实心圆来完成随着图像的滑动,这个点也随之进行相应的变化...看起来并不是什么难理解的东西...
重要的部分还是如何去实现这个过程...这个过程的实现就再下面的代码中,详细解释也在代码当中...
package com.example.picture_change; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; import java.util.Map.Entry; import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.annotation.SuppressLint; import android.app.Activity; import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager.OnPageChangeListener; import android.view.Menu; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; /* * HashMap存储的键是不允许重复的...但是值是可以重复的... * * */ public class MainActivity extends Activity { ArrayList imageSource=null; //存放图像控件... ArrayList dots=null; //存放5个点... int []images=null; //存放图像的资源... String []titles=null; //伴随着图像的变动,标题也会随之变动... TextView tv=null; //TextView来来显示title的变化... ViewPager viewpager; //ViewPager来完成滑动效果... MyPagerAdapter adapter; //适配器... Mapmap=new HashMap(); @SuppressLint("UseSparseArrays") MapmapValues=new HashMap(); private int curr=0; private int old=0; int o=0; int mapsize; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toast.makeText(MainActivity.this, "a", Toast.LENGTH_LONG).show(); /* 下面利用反射来完成Drawable的资源获取... * 在这里我获取了5张图片的资源数据..这5张图片分别为a.jpg b.jpg c.jpg d.jpg e.jpg * 这里使用了一个length<=1来完成数据的获取...其实这个方式并不好,是我自己想出来的... * 暂时没有更好的方法...我这里使用反射的目的在下面会进行介绍... * */ Field [] field=R.drawable.class.getFields(); for(Field f:field){ if(f.getName().length()<=1){ try { o++; String str="image"+"_"+o; map.put(str, f.getInt(R.drawable.class));//使用map以键值对的形式来保存图片的数据资源... } catch (IllegalArgumentException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } mapsize=map.size()-1; /* 这里我再次使用了一个map以键值对的形式只保存上一个map的Value值... * 这么做的目的在下面进行说明... * * */ for(Entry entry:map.entrySet()){ mapValues.put(mapsize, entry.getValue()); mapsize--; } init(); } public void init(){ //数据信息的初始化... images=new int[]{R.drawable.a,R.drawable.b,R.drawable.c,R.drawable.d,R.drawable.e}; titles=new String[]{"this is the one picture","this is two picture","this is three picture","this is four picture","this is five picture"}; imageSource=new ArrayList(); //这里初始化imageSource... for(int i=0;i<images.length;i++){ ImageView iamgeview =new ImageView(this); iamgeview.setBackgroundResource(images[i]); imageSource.add(iamgeview); } //这里使用了一个方法...我们没有必要一次一次的findViewById()...使用下面的方法很有效的解决了多次findViewById()函数的引用... dots=new ArrayList(); for(int j=0;j<5;j++){ String dotid="dot"+"_"+j; int resId=getResources().getIdentifier(dotid, "id", "com.example.picture_change"); dots.add(findViewById(resId)); } tv=(TextView) findViewById(R.id.tv); tv.setText(titles[0]); viewpager=(ViewPager) findViewById(R.id.vp); adapter=new MyPagerAdapter(); //这里定义了一个适配器对象... viewpager.setAdapter(adapter); //传递对象,绑定适配器... viewpager.setOnPageChangeListener(new onpagelistener()); //这里设置了一个当图片发生滑动后的一个监听效果... ScheduledExecutorService scheduled = Executors.newSingleThreadScheduledExecutor();//这里我们开启一个线程... scheduled.scheduleAtFixedRate(new Runnable() { @Override public void run() { // TODO Auto-generated method stub curr=(curr+1)%images.length; handler.sendEmptyMessage(0);//将信息发送给Handler,让Handler处理数据,完成一些操作... } }, 2, 2, TimeUnit.SECONDS); //实现内部方法,设置播放时间... } private class MyPagerAdapter extends PagerAdapter{ @Override public int getCount() { // TODO Auto-generated method stub return images.length; } @Override public boolean isViewFromObject(View arg0, Object arg1) { // TODO Auto-generated method stub //判断前后两张的显示图片是否相同... return arg0==arg1; } @Override public void destroyItem(ViewGroup container, int position, Object object) { //销毁...释放内存... container.removeView(imageSource.get(position)); } @Override public Object instantiateItem(ViewGroup container, int position) { /* 这个方法表示的是滑动到了第几张图片的定位...通过传递一个ViewGroup来完成数据的传递... * 我们上面使用到了一个Map来保存上一个Map的Value值,这个的真正目的就在这里..目的是为了 * 获取当前显示图片的资源信息..说白了就是要获取(R.drawable.属性),为什么要实现这个目的 * 因为我们要实现,当这个显示的图片被点击的时候,我们应该进行哪些操作... * */ ImageView v=imageSource.get(position);//获取当前图片... //position是从0-4的值...因此可以获取到Map中的值了... v.setClickable(true); //设置图片是可以点击的... final int values=(Integer)mapValues.get(position); //这里我们获取map中保存的Values值... System.out.println(values); //下面就是实现触发图片时的监听... v.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub switch(values){ case R.drawable.a: Toast.makeText(MainActivity.this, "a", Toast.LENGTH_LONG).show(); break; case R.drawable.b: Toast.makeText(MainActivity.this, "b", Toast.LENGTH_LONG).show(); break; case R.drawable.c: Toast.makeText(MainActivity.this, "c", Toast.LENGTH_LONG).show(); break; case R.drawable.d: Toast.makeText(MainActivity.this, "d", Toast.LENGTH_LONG).show(); break; case R.drawable.e: Toast.makeText(MainActivity.this, "e", Toast.LENGTH_LONG).show(); break; } } }); container.addView(imageSource.get(position)); //将所有的图片都加载到了container中... return imageSource.get(position); } } //定义一个内部类实现图片在变化的时候的监听... class onpagelistener implements OnPageChangeListener{ @Override public void onPageScrollStateChanged(int arg0) { // TODO Auto-generated method stub } @Override public void onPageScrolled(int arg0, float arg1, int arg2) { // TODO Auto-generated method stub } @Override public void onPageSelected(int arg0) { // TODO Auto-generated method stub //当发生滑动后,要完成的一些相应操作... tv.setText(titles[arg0]); dots.get(arg0).setBackgroundResource(R.drawable.dot); dots.get(old).setBackgroundResource(R.drawable.dot_1); old=arg0; curr=arg0; } } @SuppressLint("HandlerLeak") private Handler handler=new Handler(){ public void handleMessage(Message msg) { //接收到消息后,更新页面 viewpager.setCurrentItem(curr); }; }; @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } }
这里我使用了反射机制来获取Drawable的图像资源,然后通过switch方法来完成了当图片被点击的时候需要完成的操作...这是笔者我自己想出来的一种方法...因为Android没有提供ImageClickListener()这类的方法,因此我们只能够自己去进行书写图片被点击的方法...至于更好的方法,我是还没有发现...也是确实是能力有限制了,这个方法今天也想了整整一个下午才折腾出来的...
注意:给自己的一个提醒,HashMap的键是绝对不能够重复保存的...但是值是可以保存重复的数据的,如果保存了重复的键,那么在map只会保存第一个数据,不会对后续数据进行保存...这个也是一个很大的注意点,自己就栽这里很久,虽然很低级的错误,但是很有可能在不注意的情况下就犯下了...因此在这里也算是给自己提个醒...下次不会再犯下这样的错误的
Android图片轮播效果的几种实现方法
第一种:使用动画的方法实现:(代码繁琐)
这种发放需要:两个动画效果,一个布局,一个主类来实现,不多说了,来看代码吧:
public class IamgeTrActivity extends Activity { /** Called when the activity is first created. */ public ImageView imageView; public ImageView imageView2; public Animation animation1; public Animation animation2; public TextView text; public boolean juage = true; public int images[] = new int[] { R.drawable.icon, R.drawable.expriment, R.drawable.changer, R.drawable.dataline, R.drawable.preffitication }; public int count = 0; public Handler handler = new Handler(); public Runnable runnable = new Runnable() { @Override public void run() { // TODO Auto-generated method stub AnimationSet animationSet1 = new AnimationSet(true); AnimationSet animationSet2 = new AnimationSet(true); imageView2.setVisibility(0); TranslateAnimation ta = new TranslateAnimation( Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, -1f, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f); ta.setDuration(2000); animationSet1.addAnimation(ta); animationSet1.setFillAfter(true); ta = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 1.0f, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f); ta.setDuration(2000); animationSet2.addAnimation(ta); animationSet2.setFillAfter(true); //iamgeView 出去 imageView2 进来 imageView.startAnimation(animationSet1); imageView2.startAnimation(animationSet2); imageView.setBackgroundResource(images[count % 5]); count++; imageView2.setBackgroundResource(images[count % 5]); text.setText(String.valueOf(count)); if (juage) handler.postDelayed(runnable, 6000); Log.i(handler, handler); } }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); imageView = (ImageView) findViewById(R.id.imageView); imageView2 = (ImageView) findViewById(R.id.imageView2); text=(TextView)findViewById(R.id.text); text.setText(String.valueOf(count)); //将iamgeView先隐藏,然后显示 imageView2.setVisibility(4); handler.postDelayed(runnable, 2000); } public void onPause() { juage = false; super.onPause(); } }
布局代码:
android:orientation=vertical android:layout_width=fill_parent android:layout_height=fill_parent android:id=@+id/rl> android:id=@+id/imageView android:layout_width=fill_parent android:background=@drawable/icon android:layout_below=@+id/rl android:layout_height=120dp /> android:id=@+id/imageView2 android:layout_width=fill_parent android:background=@drawable/expriment android:layout_below=@+id/rl android:layout_height=120dp /> android:id=@+id/text android:layout_width=fill_parent android:layout_height=wrap_content android:layout_below=@id/imageView/>
第二种:使用ViewFlipper实现图片的轮播
Android系统自带的一个多页面管理控件,它可以实现子界面的自动切换:
首先 需要为ViewFlipper加入View
(1) 静态导入:在layout布局文件中直接导入
(2) 动态导入:addView()方法
ViewPlipper常用方法:
setInAnimation:设置View进入屏幕时候使用的动画
setOutAnimation:设置View退出屏幕时候使用的动画
showNext:调用该函数来显示ViewFlipper里面的下一个View
showPrevious:调用该函数来显示ViewFlipper里面的上一个View
setFlipInterval:设置View之间切换的时间间隔
startFlipping使用上面设置的时间间隔来开始切换所有的View,切换会循环进行
stopFlipping:停止View切换
讲了这么多,那么我们今天要实现的是什么呢?
(1) 利用ViewFlipper实现图片的轮播
(2) 支持手势滑动的ViewFlipper
我们需要先准备几张图片:把图片放进drawable中
创建两个动画:在res下面新建一个folder里面新建两个xml:
left_in:
android:duration=5000 android:fromXDelta=100%p android:toXDelta=0/>
left_out:
android:fromXDelta=0 android:toXDelta=-100%p android:duration=5000/>
一个布局文件:
xmlns:tools=http://schemas.android.com/tools android:layout_width=match_parent android:layout_height=match_parent tools:context=.MainActivity > android:id=@+id/flipper android:layout_width=fill_parent android:layout_height=fill_parent/>
一个主类:
public class MainActivity extends Activity { private ViewFlipper flipper; private int[] resId = {R.drawable.pc1,R.drawable.pc2,R.drawable.pc3,R.drawable.pc4}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); flipper = (ViewFlipper) findViewById(R.id.flipper); /* * 动态导入的方式为ViewFlipper加入子View * */ for (int i = 0; i < resId.length; i++) { flipper.addView(getImageView(resId[i])); } /* * 为ViewFlipper去添加动画效果 * */ flipper.setInAnimation(this, R.anim.left_in); flipper.setOutAnimation(this, R.anim.left_out); flipper.setFlipInterval(5000); flipper.startFlipping(); } private ImageView getImageView(int resId){ ImageView image = new ImageView(this); image.setBackgroundResource(resId); return image; } }
那么这样就实现了一个图片轮询的功能效果了
我们还可以添加点击,滑动效果:
我们还需要添加两个向右的滑动效果:
right_in:
android:fromXDelta=0 android:toXDelta=-100%p android:duration=2000/>
right_out:
android:fromXDelta=100%p android:toXDelta=0 android:duration=2000/>
然后我们还需要在主类里面添加(如果你不想让图片自动播放,只想通过手势来实现图片播放那么你需要把“为ViewFlipper添加动画效果的代码”删掉):
public boolean onTouchEvent(MotionEvent event) { // TODO Auto-generated method stub switch (event.getAction()) { case MotionEvent.ACTION_DOWN: startX = event.getX(); break; case MotionEvent.ACTION_MOVE://判断向左滑动还是向右滑动 if (event.getX() - startX > 100) { flipper.setInAnimation(this, R.anim.left_in); flipper.setOutAnimation(this, R.anim.left_out); flipper.showPrevious(); }else if (startX - event.getX() > 100) { flipper.setInAnimation(this, R.anim.right_in); flipper.setOutAnimation(this, R.anim.right_out); flipper.showNext(); } case MotionEvent.ACTION_UP: break; } return super.onTouchEvent(event); }
这样我们利用我们的ViewFlipper完成的图片轮询的功能就做完了。
相关文章
- 支付宝支付在国内算是大家了,我们到处都可以使用支付宝了,下文整理介绍的是在安卓app应用中使用支付宝进行支付的开发例子。 之前讲了一篇博客关与支付宝集成获取...2016-09-20
- 本文给大家分享C#连接SQL数据库和查询数据功能的操作技巧,本文通过图文并茂的形式给大家介绍的非常详细,需要的朋友参考下吧...2021-05-17
- PHP+Ajax有许多的功能都会用到它小编今天就有使用PHP+Ajax实现的一个微信登录功能了,下面我们来看一个PHP+Ajax手机发红包的程序例子,具体如下所示。 PHP发红包基本...2016-11-25
- 最基础的对数据的增加删除修改操作实例,菜鸟们收了吧...2013-09-26
- 这篇文章主要介绍了解决Mybatis 大数据量的批量insert问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-01-09
- 华为手机怎么恢复已卸载的应用?有时候我们在使用华为手机的时候,想知道卸载的应用怎么恢复,这篇文章主要介绍了华为手机恢复应用教程,需要的朋友可以参考下...2020-06-29
Antd-vue Table组件添加Click事件,实现点击某行数据教程
这篇文章主要介绍了Antd-vue Table组件添加Click事件,实现点击某行数据教程,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-11-17- 这篇文章主要介绍了详解如何清理redis集群的所有数据,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-02-18
- 这篇文章主要介绍了vue 获取到数据但却渲染不到页面上的解决方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-11-19
- 很多用安卓智能手机的朋友是用九宫格锁屏,网上也有暴力删除手机图形锁的方法,不过我们可以用程序来破解。本文只提供技术学习,不能干坏事 安卓手机的图形锁(九宫格)...2016-09-20
- 华为手机怎么开启双时钟?华为手机是可以设置双时钟的,如果来回在两个有时差的地方工作,是可以设置双时钟显示,下面我们就来看看华为添加双时钟的技巧,需要的朋友可以参考下...2020-12-08
- 在php中解析xml文档用专门的函数domdocument来处理,把json在php中也有相关的处理函数,我们要把数据xml 数据存到一个数据再用json_encode直接换成json数据就OK了。...2016-11-25
- 这篇文章主要介绍了mybatis-plus 处理大数据插入太慢的解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-12-18
安卓手机wifi打不开修复教程,安卓手机wifi打不开解决方法
手机wifi打不开?让小编来告诉你如何解决。还不知道的朋友快来看看。 手机wifi是现在生活中最常用的手机功能,但是遇到手机wifi打不开的情况该怎么办呢?如果手机wifi...2016-12-21- 这篇文章主要介绍了postgresql数据添加两个字段联合唯一的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-02-04
Vue生命周期activated之返回上一页不重新请求数据操作
这篇文章主要介绍了Vue生命周期activated之返回上一页不重新请求数据操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-07-26- 这篇文章主要介绍了解决vue watch数据的方法被调用了两次的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-11-07
- 这篇文章主要介绍了c# socket网络编程,server端接收,client端发送数据,大家参考使用吧...2020-06-25
- 这篇文章主要介绍了vue 数据(data)赋值问题的解决方案,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-03-29
- 这篇文章主要介绍了Python3 常用数据标准化方法详解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-03-24