`

gz问题

阅读更多

Android应用代码实现安装和卸载程序

 

安装:
 String str = "/CanavaCancel.apk";
 String fileName = Environment.getExternalStorageDirectory() + str;

 Intent intent = new Intent(Intent.ACTION_VIEW);

 intent.setDataAndType(Uri.fromFile(newFile(fileName)),"application/vnd.android.package-archive");

 startActivity(intent);

卸载:
 Uri packageURI = Uri.parse("package:com.demo.CanavaCancel");  

 Intent uninstallIntent = new Intent(Intent.ACTION_DELETE, packageURI);  

 startActivity(uninstallIntent);

Environment拥有一些可以获取环境变量的方法
package:com.demo.CanavaCancel 这个形式是 package:程序完整的路径 (包名+程序名).

 

 ActivityGroup简介

 

ActivityGroup简介  
1.ActivityGroup的核心就是继承了该类,能够通过getLocalActivityManager()得到一个LocalActivityManager  
如,LocalActivityManager  am = getLocalActivityManager();  

2.然后通过LocalActivityManager通过startActivity(String id, Intent intent),可以与指定的Actiivty绑定,并且返回一个Window。LocalActivityManager可以同时管理多个Activity  

Window window1 = am.startActivity("Module1", new Intent(TestView.this, ModuleView1.class));
Window window2 = am.startActivity("Module2", new Intent(TestView.this, ModuleView2.class));   

3.然后Window可以通过getDecorView()方法,返回一个View,然后通过与指定容器的addView(View)方法, 实现不同的效果  
View view1 = window1.getDecorView() View view2 = window2.getDecorView()  


实际中多用简写形式,如, 
container是ScrollView的一个实例 
container.removeAllViews();     //移除其他所有子视图 
container.addView(getLocalActivityManager().startActivity( "Module2", new Intent(TestView.this, ModuleView2.class)  .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) ) .getDecorView());  


注意: 
container.removeAllViews():表示在显示该视图之前,先移除其他所有视图. 
Intent.FLAG_ACTIVITY_CLEAR_TOP:如果在当前Task中,有要启动的Activity,那么把该Acitivity之前的所有Activity都关掉,并把此Activity置前以避免创建Activity的实例 

这种方式具有很大的灵活性,常用的就是实现TabHost分页效果,但很好的避免的TabHost的缺点,如title等

 

Android中定时器的3种实现方法

 

在Android开发中,定时器一般有以下3种实现方法:
一、采用Handler与线程的sleep(long)方法
二、采用Handler的postDelayed(Runnable, long)方法
三、采用Handler与timer及TimerTask结合的方法
下面逐一介绍:
一、采用Handle与线程的sleep(long)方法
        Handler主要用来处理接受到的消息。这只是最主要的方法,当然Handler里还有其他的方法供实现,有兴趣的可以去查API,这里不过多解释。
1. 定义一个Handler类,用于处理接受到的Message。
Handler handler = new Handler() {
    public void handleMessage(Message msg) {
        // 要做的事情
        super.handleMessage(msg);
    }
};

2. 新建一个实现Runnable接口的线程类,如下:
public class MyThread implements Runnable {
    @Override
    public void run() {
        // TODO Auto-generated method stub
        while (true) {
            try {
                Thread.sleep(10000);// 线程暂停10秒,单位毫秒
                Message message = new Message();
                message.what = 1;
                handler.sendMessage(message);// 发送消息
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}


3. 在需要启动线程的地方加入下面语句:

new Thread(new MyThread()).start();

4. 启动线程后,线程每10s发送一次消息。
二、采用Handler的postDelayed(Runnable, long)方法
这个实现比较简单一些。
1. 定义一个Handler类
Handler handler=new Handler();
Runnable runnable=new Runnable() {
    @Override
    public void run() {
        // TODO Auto-generated method stub
        //要做的事情
        handler.postDelayed(this, 2000);
    }
};


2. 启动计时器
handler.postDelayed(runnable, 2000);//每两秒执行一次runnable.

3. 停止计时器
handler.removeCallbacks(runnable);


 三、采用Handler与timer及TimerTask结合的方法
 1. 定义定时器、定时器任务及Handler句柄
private final Timer timer = new Timer();
private TimerTask task;
Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        // TODO Auto-generated method stub
        // 要做的事情
        super.handleMessage(msg);
    }
};


2. 初始化计时器任务
task = new TimerTask() {
    @Override
    public void run() {
        // TODO Auto-generated method stub
        Message message = new Message();
        message.what = 1;
        handler.sendMessage(message);
    }
};

3. 启动定时器
timer.schedule(task, 2000, 2000);

简要说一下上面三步提到的一些内容:
  1. 定时器任务(TimerTask)顾名思义,就是说当定时器到达指定的时间时要做的工作,这里是想Handler发送一个消息,由Handler类进行处理。
  2. java.util.Timer.schedule(TimerTask task, long delay):这个方法是说,dalay/1000秒后执行task.只执行一次。
      java.util.Timer.schedule(TimerTask task, long delay, long period):这个方法是说,delay/1000秒后执行task,然后进过period/1000秒再次执行task,这个用于循环任务,执行无数次,当然,你可以用timer.cancel();取消计时器的执行。

 

android.annotation.SuppressLint 报错分析


情况分析
import一个新android项目 ,报错行:
import android.annotation.SuppressLint;

提示信息:
SuppressLint cannot be resolved to a type
分析: 此问题原因是无法找到annotation包下的SuppressLint类, 推测是没有引入此jar 包.
在网上找了一下,没找到解决办法, 自己尝试解决:

解决办法:
在新项目buildPath 下, 引入android 4.2 jar , ok.
4.2 对应api 版本为17 , 在本地找到sdk 目录
android-sdks\platforms\ 如下:
可看到17 为4.2 ,打开android.jar 即为4.2 的jar.

 

 继承ViewGroup或View时:重写onMeasure方法和onLayout方法

 在继承ViewGroup类时,需要重写两个方法,分别是onMeasure和onLayout。

1,在方法onMeasure中调用setMeasuredDimension方法

void android.view.View.setMeasuredDimension(int measuredWidth, int measuredHeight)

在onMeasure(int, int)中,必须调用setMeasuredDimension(int width, int height)来存储测量得到的宽度和高度值,如果没有这么去做会触发异常IllegalStateException。

2,在方法onMeasure中调用孩子的measure方法

void android.view.View.measure(int widthMeasureSpec, int heightMeasureSpec)

这个方法用来测量出view的大小。父view使用width参数和height参数来提供constraint信息。实际上,view的测量工作在onMeasure(int, int)方法中完成。因此,只有onMeasure(int, int)方法可以且必须被重写。参数widthMeasureSpec提供view的水平空间的规格说明,参数heightMeasureSpec提供view的垂直空间的规格说明。

3,解析onMeasure(int, int)方法

void android.view.View.onMeasure(int widthMeasureSpec, int heightMeasureSpec)

测量view及其内容来确定view的宽度和高度。这个方法在measure(int, int)中被调用,必须被重写来精确和有效的测量view的内容。

在重写这个方法时,必须调用setMeasuredDimension(int, int)来存储测量得到的宽度和高度值。执行失败会触发一个IllegalStateException异常。调用父view的onMeasure(int, int)是合法有效的用法。

view的基本测量数据默认取其背景尺寸,除非允许更大的尺寸。子view必须重写onMeasure(int, int)来提供其内容更加准确的测量数值。如果被重写,子类确保测量的height和width至少是view的最小高度和宽度(通过getSuggestedMinimumHeight()和getSuggestedMinimumWidth()获取)。

4,解析onLayout(boolean, int, int, int, int)方法

void android.view.ViewGroup.onLayout(boolean changed, int l, int t, int r, int b)

调用场景:在view给其孩子设置尺寸和位置时被调用。子view,包括孩子在内,必须重写onLayout(boolean, int, int, int, int)方法,并且调用各自的layout(int, int, int, int)方法。

参数说明:参数changed表示view有新的尺寸或位置;参数l表示相对于父view的Left位置;参数t表示相对于父view的Top位置;参数r表示相对于父view的Right位置;参数b表示相对于父view的Bottom位置。.

5,解析View.MeasureSpec类

android.view.View.MeasureSpec

MeasureSpec对象,封装了layout规格说明,并且从父view传递给子view。每个MeasureSpec对象代表了width或height的规格。

MeasureSpec对象包含一个size和一个mode,其中mode可以取以下三个数值之一:

UNSPECIFIED,1073741824 [0x40000000],未加规定的,表示没有给子view添加任何规定。父布局没有给子布局任何限制,子布局可以任意大小
EXACTLY,0 [0x0],精确的,表示父view为子view确定精确的尺寸。 父布局决定子布局的确切大小。不论子布局多大,它都必须限制在这个界限里。
AT_MOST,-2147483648 [0x80000000],子view可以在指定的尺寸内尽量大。子布局可以根据自己的大小选择任意大小。

Android下如何理解onMeasure,onLayout的过程

在Android中view如何完成绘制这个过程介绍了很多,但是很多理论化的东西,最近重新整理一下,通俗的讲解一下。
  View绘制过程就好比你向银行贷款,
  在执行onMeasure的时候,好比银行告诉你大概贷款额度有多少?你根据自己的需求,进行各方面的计算,计算出一个自己大概需要的金额,然后告诉询问需要多少贷款。贷款额度好比显示空间大小。
  实际代码执行过程是这样
  onMeasure(int widthMeasureSpec, int heightMeasureSpec),其中widthMeasureSpec和heightMeasureSpec是银行告诉的大致额度。然后内部计算完成后,通过setMeasuredDimension(width, height)将实际需要的大小返回给父view。
  即onMeasure用来确定确定view显示的大小(通过调用子view的measure,来确定子view的大小)
  在执行onLayout的时候,好比银行收到你的贷款请求后,根据自身储备的情况及你的资质情况,批发了贷款的额度及领取的时间。领取的时间好比显示的起始位置,额度好比是显示空间的大小。
  实际代码执行过程是这样的
  onLayout(boolean changed, int left, int top, int right, int bottom)-确定view在父类中的显示位置,通过对子view的位置计算,通过调用子view的layout将在父类中的位置设置给子view。
  在执行dispatchDraw的时候,好比你去银行真正获取贷款,拿到你需要的钱了,签署各类合同和手续,完成你的贷款过程。
  在dispatchDraw的过程是在指定的空间内绘制你需要绘制的内容,可以通过drawChild实现内部子view的绘制。

 完全自定义控件

通常是继承View或者SurfaceView ,View类提供一个Canvas(画布)和一系列的画的方法,还有Paint(画笔)。使用它们去创建一个自定义的UI。你可以重写事件,包括屏幕接触或者按键按下等等,用来提供与用户交互。

1.如果你不需要快速重画和3D图像的效果,那么让View作为父类提供一个轻量级的解决方案。

2.如若不然,就需要使用SurfaceView作为父类,这样你就可以提供一个后台线程去画和使用OPENGL去实现你的图像。这个就相对重量级了,如果你的视图需要经常更新,然后由需要显示比较复杂的图像信息(尤其是在游戏和3D可视化),SurfaceView将是更好的选择。

在这里我们讨论前者,后者后期再讨论。

 一般你需要重写2个方法: 
1.onMeasure

什么是onMeasure?

下面转载一段文章:

View在屏幕上显示出来要先经过measure(计算)和layout(布局). 
1、什么时候调用onMeasure方法?  
当控件的父元素正要放置该控件时调用.父元素会问子控件一个问题,“你想要用多大地方啊?”,然后传入两个参数——widthMeasureSpec和heightMeasureSpec. 
两个参数指明控件可获得的空间以及关于这个空间描述的元数据.

更好的方法是你传递View的高度和宽度到setMeasuredDimension方法里,这样可以直接告诉父控件,需要多大地方放置子控件.

    widthMeasureSpec和heightMeasureSpec这2个参数都是整形是出于效率的考虑,所以经常要做的就是对其解码=>

  1. int specMode = MeasureSpec.getMode(measureSpec);
  2. int specSize = MeasureSpec.getSize(measureSpec);

  

  1. 依据specMode的值,(MeasureSpec有3种模式分别是UNSPECIFIED, EXACTLY和AT_MOST
  2. 如果是AT_MOST,specSize 代表的是最大可获得的空间; 
    如果是EXACTLY,specSize 代表的是精确的尺寸; 
    如果是UNSPECIFIED,对于控件尺寸来说,没有任何参考意义。 
    2、那么这些模式和我们平时设置的layout参数fill_parent, wrap_content有什么关系呢? 
    经过代码测试就知道,当我们设置width或height为fill_parent时,容器在布局时调用子 view的measure方法传入的模式是EXACTLY,因为子view会占据剩余容器的空间,所以它大小是确定的。 
    而当设置为 wrap_content时,容器传进去的是AT_MOST, 表示子view的大小最多是多少,这样子view会根据这个上限来设置自己的尺寸。当子view的大小设置为精确值时,容器传入的是EXACTLY, 而MeasureSpec的UNSPECIFIED模式表示你没有指定大小。
  3. View的onMeasure方法默认行为是当模式为UNSPECIFIED时,设置尺寸为mMinWidth(通常为0)或者背景drawable的最小尺寸,当模式为EXACTLY或者AT_MOST时,尺寸设置为传入的MeasureSpec的大小。  
    有个观念需要纠正的是,fill_parent应该是子view会占据剩下容器的空间,而不会覆盖前面已布局好的其他view空间,当然后面布局子 view就没有空间给分配了,所以fill_parent属性对布局顺序很重要。以前所想的是把所有容器的空间都占满了,难怪google在2.2版本里 把fill_parent的名字改为match_parent. 
    在两种情况下,你必须绝对的处理这些限制。在一些情况下,它可能会返回超出这些限制的尺寸,在这种情况下,你可以让父元素选择如何对待超出的View,使用裁剪还是滚动等技术。 
    接下来的框架代码给出了处理View测量的典型实现:

      

01 @Override
02     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
03         int measuredHeight = measureHeight(heightMeasureSpec);
04         int measuredWidth = measureWidth(widthMeasureSpec);
05  
06         setMeasuredDimension(measuredHeight, measuredWidth); // 记住这句可不能省。
07     }
08  
09     private int measureHeight(int measureSpec) {
10         int specMode = MeasureSpec.getMode(measureSpec);
11         int specSize = MeasureSpec.getSize(measureSpec);
12  
13         // Default size if no limits are specified.
14         int result = 500;
15  
16         if (specMode == MeasureSpec.AT_MOST) {
17             // Calculate the ideal size of your
18             // control within this maximum size.
19             // If your control fills the available
20             // space return the outer bound.
21             result = specSize;
22         } else if (specMode == MeasureSpec.EXACTLY) {
23             // If your control can fit within these bounds return that value.
24             result = specSize;
25         }
26         return result;
27     }
28  
29     private int measureWidth(int measureSpec) {
30         // 代码基本类似measureHeight
31     }

       总结:

通过

            int specMode = MeasureSpec.getMode(measureSpec); 
            int specSize = MeasureSpec.getSize(measureSpec);

这2个值,然后计算自己想要占有的宽和高

 

 Activity Task 与 Intent Filter Flag

 

 一、Activity和Task(栈)的关系
  Task就像一个容器,而Activity就相当与填充这个容器的东西,第一个东西(Activity)则会处于最下面,最后添加的东西(Activity)则会在最上面。从Task中取出东西(Activity)是从最顶端取出,也就是说最先取出的是最后添加的东西(Activity),以此类推,最后取出的是第一次添加的Activity,而Activity在Task中的顺序是可以控制的,在Activity跳转时用到Intent Flag可以设置新建activity的创建方式;
二、具体Intent用法如下:
  //默认的跳转类型,会重新创建一个新的Activity
  intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  //如果activity在task存在,拿到最顶端,不会启动新的Activity
  intent.addFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT);
  //如果activity在task存在,将Activity之上的所有Activity结束掉
  intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
  //如果Activity已经运行到了Task,再次跳转不会启动新的Activity
  intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);

什么是Affinity

在某些情况下,Android需要知道一个Activity属于哪个Task,即使它没有被启动到一个具体的Task里。这是通过任务共用性(Affinities)完成的。任务共用性(Affinities)为这个运行一个或多个Activity的Task提供了一个独特的静态名称,默认的一个活动的任务共用性(Affinity)是实现了该Activity的.apk包的名字。
当开始一个没有Intent.FLAG_ACTIVITY_NEW_TASK标志的Activity时,任务共用性affinities不会影响将会运行该新活动的Task:它总是运行在启动它的Task里。但是,如果使用了NEW_TASK标志,那么共用性(affinity)将被用来判断是否已经存在一个有相同共用性(affinity)的Task。如果是这样,这项Task将被切换到前面而新的Activity会启动于这个Task的顶层。
这种特性在您必须使用NEW_TASK标志的情况下最有用,尤其是从状态栏通知或桌面快捷方式启动活动时。结果是,当用户用这种方式启动您的应用程序时,它的当前Task将被切换到前台,而且想要查看的Activity被放在最上面。
你可以在程序清单(Manifest)文件的应用程序application标签中为.apk包中所有的活动分配你自己的任务共用性Affinites,或者在活动标记中为各个活动进行分配。
一些说明其如何使用的例子如下:
如果您的.apk包含多个用户可以启动的高层应用程序,那么您可能需要对用户看到的每个Activity(活动)指定不同的affinities。一个不错的命名惯例是以附加一个以冒号分隔的字符串来扩展您的.apk包名。例如,“ com.android.contacts ”.apk可以有affinities:“com.android.contacts:Dialer”和“ com.android.contacts:ContactsList”。
如果您正在替换一个通知,快捷方式,或其他可以从外部发起的应用程序的“内部”活动,你可能需要明确设定您替代活动的taskAffinity和您准备替代的应用程序一样。例如,如果您想替换contacts详细信息视图(用户可以创建并调用快捷方式),你得把taskAffinity设置成“com.android.contacts”。
 
在前面的文章“Android四种Activity的加载模式”我们提到:Activity的加载模式受启动Activity的Intent对象中设置的Flag和manifest文件中Activity的<activity>元素的特性值交互控制。
 
跟 Task 有关的 manifest文件中Activity的特性值介绍

android:allowTaskReparenting
    用来标记Activity能否从启动的Task移动到有着affinity的Task(当这个Task进入到前台时)
   “true”,表示能移动,“false”,表示它必须呆在启动时呆在的那个Task里。
    如果这个特性没有被设定,设定到<application>元素上的allowTaskReparenting特性的值会应用到Activity上。默认值为“false”。
    一般来说,当Activity启动后,它就与启动它的Task关联,并且在那里耗尽它的整个生命周期。当当前的Task不再显示时,你可以使用这个特性来强制Activity移动到有着affinity的Task中。典型用法是:把一个应用程序的Activity移到另一个应用程序的主Task中。
    例如,如果 email中包含一个web页的链接,点击它就会启动一个Activity来显示这个页面。这个Activity是由Browser应用程序定义的,但是,现在它作为email Task的一部分。如果它重新宿主到Browser Task里,当Browser下一次进入到前台时,它就能被看见,并且,当email Task再次进入前台时,就看不到它了。
    Actvity的affinity是由taskAffinity特性定义的。Task的affinity是通过读取根Activity的affinity决定。因此,根Activity总是位于相同affinity的Task里。由于启动模式为“singleTask”和“singleInstance”的Activity只能位于Task的底部,因此,重新宿主只能限于“standard”和“singleTop”模式。
android:alwaysRetainTaskState
    用来标记Activity所在的Task的状态是否总是由系统来保持。
    “true”,表示总是;“false”,表示在某种情形下允许系统恢复Task到它的初始化状态。默认值是“false”。
    这个特性只针对Task的根Activity有意义;对其它Activity来说,忽略之。
    一般来说,特定的情形如当用户从主画面重新选择这个Task时,系统会对这个Task进行清理(从stack中删除位于根Activity之上的所有Activivity)。典型的情况,当用户有一段时间没有访问这个Task时也会这么做,例如30分钟。
    然而,当这个特性设为“true”时,用户总是能回到这个Task的最新状态,无论他们是如何启动的。这非常有用,例如,像Browser应用程序,这里有很多的状态(例如多个打开的Tab),用户不想丢失这些状态。
android:clearTaskOnLaunch
    用来标记是否从Task中清除所有的Activity,除了根Activity外(每当从主画面重新启动时)
   “true”,表示总是清除至它的根Activity,“false”表示不。默认值是“false”。
    这个特性只对启动一个新的Task的Activity(根Activity)有意义;对Task中其它的Activity忽略。
    当这个值为“true”,每次用户重新启动这个Task时,都会进入到它的根Activity中,不管这个Task最后在做些什么,也不管用户是使用BACK还是HOME离开的。当这个值为“false”时,可能会在一些情形下(参考alwaysRetainTaskState特性)清除Task的Activity,但不总是。
    假设,某人从主画面启动了Activity P,并从那里迁移至Activity Q。接下来用户按下HOME,然后返回Activity P。一般,用户可能见到的是Activity Q,因为它是P的Task中最后工作的内容。然而,如果P设定这个特性为“true”,当用户按下HOME并使这个Task再次进入前台时,其上的所有的Activity(在这里是Q)都将被清除。因此,当返回到这个Task时,用户只能看到P。
    如果这个特性和allowTaskReparenting都设定为“true”,那些能重新宿主的Activity会移动到共享affinity的Task中;剩下的Activity都将被抛弃,如上所述。
android:finishOnTaskLaunch
    用来标记当用户再次启动它的Task(在主画面选择这个Task)时已经存在的Activity实例是否要关闭(结束)
   “true”,表示应该关闭,“false”表示不关闭。默认值是“false”。
    如果这个特性和allowTaskReparenting都设定为“true”,这个特性胜出。Activity的affinity忽略。这个Activity不会重新宿主,但是会销毁。
android:launchMode
    用于指示Activity如何启动。这里有四种模式,与Intent对象中的Activity Flags(FLAG_ACTIVITY_*变量)共同作用,来决定Activity如何启动来处理Intent。它们是:
    "standard"
    "singleTop"
    "singleTask"
    "singleInstance"
    默认模式是“standard”。
    前面文章:“Android四种Activity的加载模式”已经详细描述,这里就不做描述了.
android:noHistory
    用于标记当用户从Activity上离开并且它在屏幕上不再可见时Activity是否从Activity stack中清除并结束(调用finish()方法)——“true”,表示它应该关闭,“false”,表示不需要。默认值是“false”。
    “true”值意味着Activity不会留下历史痕迹。因为它不会在Activity stack的Task中保留,因此,用户不能返回它。
    比如启用界面的就可以借用这个。
android:taskAffinity
   这就是本文所描述的任务共用性。
   Activity为Task拥有的一个affinity。拥有相同的affinity的Activity理论上属于相同的Task(在用户的角度是相同的“应用程序”)。Task的affinity是由它的根Activity决定的。
   affinity决定两件事情——Activity重新宿主的Task(参考allowTaskReparenting特性)和使用FLAG_ACTIVITY_NEW_TASK标志启动的Activity宿主的Task。
    默认情况,一个应用程序中的所有Activity都拥有相同的affinity。捏可以设定这个特性来重组它们,甚至可以把不同应用程序中定义的Activity放置到相同的Task中。为了明确Activity不宿主特定的Task,设定该特性为空的字符串。
    如果这个特性没有设置,Activity将从应用程序的设定那里继承下来(参考<application>元素的taskAffinity特性)。应用程序默认的affinity的名字是<manifest>元素中设定的package名。
 
跟 Task 有关的 Intent对象中设置的Flag

FLAG_ACTIVITY_BROUGHT_TO_FRONT
    这个标志一般不是由程序代码设置的,如在launchMode中设置singleTask模式时系统帮你设定。
FLAG_ACTIVITY_CLEAR_TOP
    如果设置,并且这个Activity已经在当前的Task中运行,因此,不再是重新启动一个这个Activity的实例,而是在这个Activity上方的所有Activity都将关闭,然后这个Intent会作为一个新的Intent投递到老的Activity(现在位于顶端)中。
    例如,假设一个Task中包含这些Activity:A,B,C,D。如果D调用了startActivity(),并且包含一个指向Activity B的Intent,那么,C和D都将结束,然后B接收到这个Intent,因此,目前stack的状况是:A,B。
    上例中正在运行的Activity B既可以在onNewIntent()中接收到这个新的Intent,也可以把自己关闭然后重新启动来接收这个Intent。如果它的启动模式声明为“multiple”(默认值),并且你没有在这个Intent中设置FLAG_ACTIVITY_SINGLE_TOP标志,那么它将关闭然后重新创建;对于其它的启动模式,或者在这个Intent中设置FLAG_ACTIVITY_SINGLE_TOP标志,都将把这个Intent投递到当前这个实例的onNewIntent()中。
    这个启动模式还可以与FLAG_ACTIVITY_NEW_TASK结合起来使用:用于启动一个Task中的根Activity,它会把那个Task中任何运行的实例带入前台,然后清除它直到根Activity。这非常有用,例如,当从Notification Manager处启动一个Activity。
FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET
    如果设置,这将在Task的Activity stack中设置一个还原点,当Task恢复时,需要清理Activity。也就是说,下一次Task带着FLAG_ACTIVITY_RESET_TASK_IF_NEEDED标记进入前台时(典型的操作是用户在主画面重启它),这个Activity和它之上的都将关闭,以至于用户不能再返回到它们,但是可以回到之前的Activity。
    这在你的程序有分割点的时候很有用。例如,一个e-mail应用程序可能有一个操作是查看一个附件,需要启动图片浏览Activity来显示。这个Activity应该作为e-mail应用程序Task的一部分,因为这是用户在这个Task中触发的操作。然而,当用户离开这个Task,然后从主画面选择e-mail app,我们可能希望回到查看的会话中,但不是查看图片附件,因为这让人困惑。通过在启动图片浏览时设定这个标志,浏览及其它启动的Activity在下次用户返回到mail程序时都将全部清除。
FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
    如果设置,新的Activity不会在最近启动的Activity的列表中保存。
FLAG_ACTIVITY_FORWARD_RESULT
    如果设置,并且这个Intent用于从一个存在的Activity启动一个新的Activity,那么,这个作为答复目标的Activity将会传到这个新的Activity中。这种方式下,新的Activity可以调用setResult(int),并且这个结果值将发送给那个作为答复目标的Activity。
FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
    这个标志一般不由应用程序代码设置,如果这个Activity是从历史记录里启动的(常按HOME键),那么,系统会帮你设定。
FLAG_ACTIVITY_MULTIPLE_TASK
    不要使用这个标志,除非你自己实现了应用程序启动器。与FLAG_ACTIVITY_NEW_TASK结合起来使用,可以禁用把已存的Task送入前台的行为。当设置时,新的Task总是会启动来处理Intent,而不管这是是否已经有一个Task可以处理相同的事情。
    由于默认的系统不包含图形Task管理功能,因此,你不应该使用这个标志,除非你提供给用户一种方式可以返回到已经启动的Task。
    如果FLAG_ACTIVITY_NEW_TASK标志没有设置,这个标志被忽略。
FLAG_ACTIVITY_NEW_TASK
    如果设置,这个Activity会成为历史stack中一个新Task的开始。一个Task(从启动它的Activity到下一个Task中的Activity)定义了用户可以迁移的Activity原子组。Task可以移动到前台和后台;在某个特定Task中的所有Activity总是保持相同的次序。
    这个标志一般用于呈现“启动”类型的行为:它们提供用户一系列可以单独完成的事情,与启动它们的Activity完全无关。
    使用这个标志,如果正在启动的Activity的Task已经在运行的话,那么,新的Activity将不会启动;代替的,当前Task会简单的移入前台。参考FLAG_ACTIVITY_MULTIPLE_TASK标志,可以禁用这一行为。
    这个标志不能用于调用方对已经启动的Activity请求结果。
FLAG_ACTIVITY_NO_ANIMATION
    如果在Intent中设置,并传递给Context.startActivity()的话,这个标志将阻止系统进入下一个Activity时应用Acitivity迁移动画。这并不意味着动画将永不运行——如果另一个Activity在启动显示之前,没有指定这个标志,那么,动画将被应用。这个标志可以很好的用于执行一连串的操作,而动画被看作是更高一级的事件的驱动。
FLAG_ACTIVITY_NO_HISTORY
    如果设置,新的Activity将不再历史stack中保留。用户一离开它,这个Activity就关闭了。这也可以通过设置noHistory特性。
FLAG_ACTIVITY_NO_USER_ACTION
    如果设置,作为新启动的Activity进入前台时,这个标志将在Activity暂停之前阻止从最前方的Activity回调的onUserLeaveHint()。
    典型的,一个Activity可以依赖这个回调指明显式的用户动作引起的Activity移出后台。这个回调在Activity的生命周期中标记一个合适的点,并关闭一些Notification。
    如果一个Activity通过非用户驱动的事件,如来电或闹钟,启动的,这个标志也应该传递给Context.startActivity,保证暂停的Activity不认为用户已经知晓其Notification。
FLAG_ACTIVITY_PREVIOUS_IS_TOP
    If set and this intent is being used to launch a new activity from an existing one, the current activity will not be counted as the top activity for deciding whether the new intent should be delivered to the top instead of starting a new one. The previous activity will be used as the top, with the assumption being that the current activity will finish itself immediately.
FLAG_ACTIVITY_REORDER_TO_FRONT
    如果在Intent中设置,并传递给Context.startActivity(),这个标志将引发已经运行的Activity移动到历史stack的顶端。
    例如,假设一个Task由四个Activity组成:A,B,C,D。如果D调用startActivity()来启动Activity B,那么,B会移动到历史stack的顶端,现在的次序变成A,C,D,B。如果FLAG_ACTIVITY_CLEAR_TOP标志也设置的话,那么这个标志将被忽略。
FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
If set, and this activity is either being started in a new task or bringing to the top an existing task, then it will be launched as the front door of the task. This will result in the application of any affinities needed to have that task in the proper state (either moving activities to or from it), or simply resetting that task to its initial state if needed.
FLAG_ACTIVITY_SINGLE_TOP
    如果设置,当这个Activity位于历史stack的顶端运行时,不再启动一个新的。

 

 Activity的声明周期Task

 以2个Activity为例
第一个为A1,第二个为A2。
第一次试验,A1使用startActivity跳转到A2,A2使用返回键回到A1.A1锁频,A1解除锁频,A1退出
从A1启动:
A1:Create    这里是所有Activity的入口函数,第一次被创建会调用。
A1: Start     
A1: Resume
    Activity栈中只有:   A1
从A1跳转到A2:跳转方式为startActivity(Intent);
A1: Pause    首先暂停暂停当前Activity-A1
A2:Create    创建A2
A2: Start
A2: Resume
A1: Stop     当A2创建完毕,将A1停止。但是没有调用Destroy,A1扔保留在Activity的栈中。
    Activity栈中有:   A1,A2
从A2按返回键,回到A1。
A2: Pause    首先暂停当前Activity-A2
A1: Restart  因为A1未被销毁,所以无需重新创建,调用onCreate,直接从Restart开始。
A1: Start     
A1: Resume
A2: Stop     当A1创建完毕,将A2停止。
A2: Destory当使用返回键回退时,Activity栈会将A2弹出栈,所以此时A2的onDestroy会被调用。
   Activity栈中有:   A1
A1界面时,长时间放置或使用锁屏键进行锁屏
            A1: Pause
解除锁屏
            A1: Resume
A1使用返回键退出程序
A1: Pause
A1: Stop 
A1: Destory  A1被弹出栈
    Activity栈中有:  空
第二次试验,A1使用startActivity+finish()跳转到A2,在A2界面使用返回键,回退到桌面。
从A1启动:
A1:Create    这里是所有Activity的入口函数,第一次被创建会调用。
A1: Start     
A1: Resume
    Activity栈中只有:   A1
从A1跳转到A2:跳转方式为startActivity(Intent);
A1: Pause    首先暂停暂停当前Activity-A1
A2:Create    创建A2
A2: Start
A2: Resume
A1: Stop     当A2创建完毕,将A1停止。但是没有调用Destroy,A1扔保留在Activity的栈中。
A1: Destory   finish会调用当前Activity的onDestroy函数,将当前Activity销毁。
    Activity栈中有:   A2
从A2按返回键,将会回到进入程序时的界面。因为按返回键,将当前Activity弹出,栈中空了。
A2: Pause    首先暂停当前Activity-A2
A2: Stop    
A2: Destory当使用返回键回退时,Activity栈会将A2弹出栈,所以此时A2的onDestroy会被调用。
   Activity栈中有:   空

实例总结:
    1.使用startActivity跳转,在第二个页面创建完毕后系统才会调用onStop(),并不会调用onDestroy()。因为Activity并没有被弹出栈,而是不停的被压入栈。
2.使用返回键回到上一个Activity时,系统会调用当前Activity的onDestroy(),将当前activity弹出栈。
3.当调用finish方法后,系统并非马上调用当前Activity的onDestroy。而是先执行后边的代码。
finish();
Intent jumpIntent = new Intent(MainActivity.this,SecondActivity.class);
startActivity(jumpIntent);
Intent jumpIntent = new Intent(MainActivity.this,SecondActivity.class);
startActivity(jumpIntent);
finish();
两种代码执行效果是一样的。
4、手动调用onDestroy等操作,与系统调用不同。手动调用是无效的。也就是说,当进行跳转时,手动调用了onDestroy(),Activity仍然会在栈中,而且也仍然会执行onDestroy之后的代码。
5、Finish()与onDestroy
Finish() 在你的activity动作完成的时候,或者Activity需要关闭的时候,调用此方法。当你调用此方法的时候,系统只是将最上面的Activity移出了栈,并没有及时的调用onDestory()方法,其占用的资源也没有被及时释放。
onDestroy() 系统销毁了这个Activity的实例在内存中占据的空间。在Activity的生命周期中,onDestory()方法是他生命的最后一步,资源空间什么的都没有咯~~。当重新进入此Activity的时候,必须重新创建,执行onCreate()方法。

http://blog.csdn.net/coding_glacier/article/details/7415016很详细

 

 android 启动第三方程序的代码(利用ComponentName)

 

在Intent中可以直接使用Intent.setClass()

也可以使用组件Component

简单的例子:


package com.ccp;   
 

import android.app.Activity;  
import android.content.ComponentName;  
import android.content.Intent;  
import android.os.Bundle;  
import android.view.View;  
import android.view.View.OnClickListener;  
import android.widget.Button;   
 
public class Test extends Activity {   
    /** Called when the activity is first created. */  
    @Override  
    public void onCreate(Bundle savedInstanceState) {   
        super.onCreate(savedInstanceState);   
       setContentView(R.layout.main);   
        Button btn = (Button) findViewById(R.id.btn);   
        btn.setOnClickListener(new OnClickListener() {   
               
            @Override  
            public void onClick(View v) {  
//组件名称,第一个参数是包名,也是主配置文件Manifest里设置好的包名  
第二个是类名,要带上包名   
  
                ComponentName com = new ComponentName("com.ccp", "com.ccp.Test1");   
                Intent  intent = new Intent();  
//设置部件   
                intent.setComponent(com);   
                Test.this.startActivity(intent);   
            }   
        });   
    }  

android 启动第三方程序的代码

方法一:
Intent intent = new Intent(); 
intent.setClassName(<package name>, <class name>); 
startActivity(intent);
 
方法二:
Intent i=new Intent;
ComponentName com= new ComponentName(<Package Name> , <Calss Name>); 
i.setComponent(com); 
startActivity(i); 
//启动媒体库


Intent i = new Intent();

ComponentName comp = new ComponentName("com.android.camera","com.android.camera.GalleryPicker");

i.setComponent(comp);

i.setAction("android.intent.action.VIEW");

startActivity(i);

 

//启动相机


Intent mIntent = new Intent();

ComponentName comp = new ComponentName("com.android.camera","com.android.camera.Camera");

mIntent.setComponent(comp);

mIntent.setAction("android.intent.action.VIEW");

startActivity(mIntent);

 

//启动htmlviewer,并打开指定的一个文件 注意TXT不能是ANSI的,否则会乱码


Intent intent = new Intent();

ComponentName cn = new ComponentName("com.android.htmlviewer", "com.android.htmlviewer.HTMLViewerActivity");

intent.setComponent(cn);

Uri uri = Uri.fromFile(new File("/sdcard/demo.txt"));

intent.setDataAndType(uri, "text/plain");

startActivity(intent);

 

 

Environment 是一个提供访问环境变量的类

 

Environment 包含常量

 

MEDIA_BAD_REMOVAL

解释:返回getExternalStorageState() ,表明SDCard 被卸载前己被移除

MEDIA_CHECKING

解释:返回getExternalStorageState() ,表明对象正在磁盘检查。

MEDIA_MOUNTED

解释:返回getExternalStorageState() ,表明对象是否存在并具有读/写权限

MEDIA_MOUNTED_READ_ONLY

解释:返回getExternalStorageState() ,表明对象权限为只读

MEDIA_NOFS

解释:返回getExternalStorageState() ,表明对象为空白或正在使用不受支持的文件系统。

MEDIA_REMOVED

解释:返回getExternalStorageState() ,如果不存在 SDCard 返回

MEDIA_SHARED

解释:返回getExternalStorageState() ,如果 SDCard 未安装 ,并通过 USB 大容量存储共享 返回

MEDIA_UNMOUNTABLE

解释:返回getExternalStorageState() ,返回 SDCard 不可被安装 如果 SDCard 是存在但不可以被安装

MEDIA_UNMOUNTED

解释:返回getExternalStorageState() ,返回 SDCard 已卸掉如果 SDCard 是存在但是没有被安装


Environment 常用方法

 

方法:getDataDirectory()
解释:返回 File ,获取 Android 数据目录。

方法:getDownloadCacheDirectory()
解释:返回 File ,获取 Android 下载/缓存内容目录。

方法:getExternalStorageDirectory()
解释:返回 File ,获取外部存储目录即 SDCard

方法:getExternalStoragePublicDirectory(String type)
解释:返回 File ,取一个高端的公用的外部存储器目录来摆放某些类型的文件

方法:getExternalStorageState()
解释:返回 File ,获取外部存储设备的当前状态

方法:getRootDirectory()
解释:返回 File ,获取 Android 的根目录

 

 ExpandableListView(扩展列表)的使用例子

extends ListView

main.xml的布局很简单,只有一个ExpandableListView

<!-- android:cacheColorHint="#00000000" 去除拖动view时背景变成黑色的效果 -->
<!-- android:listSelector="#00000000" 去除选中时的底色 -->
 <ExpandableListView android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:id="@+id/expandableList"
            ></ExpandableListView>
/////////////////////Actvity----------------------------

public class MainActivity extends Activity {

 private ExpandableListView listView;
 private MyAdapter adapter;
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  //隐藏标题
  requestWindowFeature(Window.FEATURE_NO_TITLE);
  setContentView(R.layout.activity_main);
  adapter = new MyAdapter(this);
  
  listView = (ExpandableListView)findViewById(R.id.expandableList);
  listView.setAdapter(adapter);
  listView.setOnChildClickListener(childClick);
  
 }
 private OnChildClickListener childClick = new OnChildClickListener() {
  
  @Override
  public boolean onChildClick(ExpandableListView parent, View v,
    int groupPosition, int childPosition, long id) {
   
   Toast.makeText(MainActivity.this, "你点击了"+adapter.getChild(groupPosition, childPosition), Toast.LENGTH_SHORT).show();
   
   return false;
  }
 };

}

 

//////////////////////自定义Adapter-------------------------

public class MyAdapter extends BaseExpandableListAdapter {
 /**
  * 设置组视图的图片
  */
 int[] logos = new int[]{R.drawable.ic_launcher,R.drawable.ic_launcher};
 /**
  * 设置组视图的显示文字
  */
 private String[] generalsTypes= new String[]{"魏","蜀"};
 /**
  * 子视图显示文字
  */
 private String[][] generals = new String[][]{
   {"A","B","C"},{"D","E","F"} 
 };
 /**
  * 子视图图片
  */
 private int[][] generallogos = new int[][]{
   {R.drawable.ic_launcher,R.drawable.ic_launcher,R.drawable.ic_launcher},
   {R.drawable.ic_launcher,R.drawable.ic_launcher,R.drawable.ic_launcher}
   
 };
 private List<String> list;
 private Context mContext;
 LayoutInflater inflater;
 
 public MyAdapter(Context context) {

  this.mContext = context;
 }
 
 //自定义一个获得文字信息的方法
 TextView getTextView(){
  AbsListView.LayoutParams lp = new AbsListView.LayoutParams(
    ViewGroup.LayoutParams.FILL_PARENT,64);
  
  TextView textView = new TextView(mContext);
  textView.setLayoutParams(lp);
  textView.setGravity(Gravity.CENTER_VERTICAL);
  textView.setPadding(36, 0, 0, 0);
  textView.setTextSize(20);
  textView.setTextColor(Color.BLACK);
  return textView;
  
 }
 
 
 
 @Override
 public Object getChild(int groupPosition, int childPosition) {
  // TODO Auto-generated method stub
  return generals[groupPosition][childPosition];
 }
 @Override
 public long getChildId(int groupPosition, int childPosition) {
  // TODO Auto-generated method stub
  return childPosition;
 }
 
 //-----------------------------------------
 @Override
 public View getChildView(int groupPosition, int childPosition,
   boolean isLastChild, View convertView, ViewGroup parent) {
  
  LinearLayout ll = new LinearLayout(mContext);
  ll.setOrientation(0);
  ImageView generallogo = new ImageView(mContext);
  generallogo.setImageResource(generallogos[groupPosition][childPosition]);;
  ll.addView(generallogo);
  
  TextView textView = getTextView();
  textView.setText(getChild(groupPosition, childPosition).toString());
  
  ll.addView(textView);
  return ll;
 }
 @Override
 public View getGroupView(int groupPosition, boolean isExpanded,
   View convertView, ViewGroup parent) {
  LinearLayout ll = new LinearLayout(mContext);
        ll.setOrientation(0);
        ImageView logo = new ImageView(mContext);
        logo.setImageResource(logos[groupPosition]);
        logo.setPadding(50, 0, 0, 0);
        ll.addView(logo);
        TextView textView = getTextView();
        textView.setTextColor(Color.BLACK);
        textView.setText(getGroup(groupPosition).toString());
        ll.addView(textView);
  return ll;
 }
 
 //---------------------------------------------------
 @Override
 public int getChildrenCount(int groupPosition) {
  // TODO Auto-generated method stub
  return generals[groupPosition].length;
 }
 @Override
 public Object getGroup(int groupPosition) {
  // TODO Auto-generated method stub
  return generalsTypes[groupPosition];
 }
 @Override
 public int getGroupCount() {
  // TODO Auto-generated method stub
  return generalsTypes.length;
 }
 @Override
 public long getGroupId(int groupPosition) {
  // TODO Auto-generated method stub
  return groupPosition;
 }
 
 @Override
 public boolean hasStableIds() {
  // TODO Auto-generated method stub
  return true;
 }
 @Override
 public boolean isChildSelectable(int groupPosition, int childPosition) {
  // TODO Auto-generated method stub
  return true;
 }
}

 

Android Wifi方法大全

 

在Android里,所有的wifi操作都在android.net.wifi包里

public class WifiAdmin    
{    
    //定义WifiManager对象    
    private WifiManager mWifiManager;    
    //定义WifiInfo对象    
    private WifiInfo mWifiInfo;    
    //扫描出的网络连接列表    
    private List<ScanResult> mWifiList;    
    //网络连接列表    
    private List<WifiConfiguration> mWifiConfiguration;    
    //定义一个WifiLock    
    WifiLock mWifiLock;    
    //构造器    
    public  WifiAdmin(Context context)    
    {    
        //取得WifiManager对象    
        mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);    
        //取得WifiInfo对象    
        mWifiInfo = mWifiManager.getConnectionInfo();    
    }    
    //打开WIFI    
    public void OpenWifi()    
    {    
        if (!mWifiManager.isWifiEnabled())    
        {    
            mWifiManager.setWifiEnabled(true);    
                
        }    
    }    
    //关闭WIFI    
    public void CloseWifi()    
    {    
        if (!mWifiManager.isWifiEnabled())    
        {    
            mWifiManager.setWifiEnabled(false);     
        }    
    }    
    //锁定WifiLock    
    public void AcquireWifiLock()    
    {    
        mWifiLock.acquire();    
    }    
    //解锁WifiLock    
    public void ReleaseWifiLock()    
    {    
        //判断时候锁定    
        if (mWifiLock.isHeld())    
        {    
            mWifiLock.acquire();    
        }    
    }    
    //创建一个WifiLock    
    public void CreatWifiLock()    
    {    
        mWifiLock = mWifiManager.createWifiLock("Test");    
    }    
    //得到配置好的网络    
    public List<WifiConfiguration> GetConfiguration()    
    {    
        return mWifiConfiguration;    
    }    
    //指定配置好的网络进行连接    
    public void ConnectConfiguration(int index)    
    {    
        //索引大于配置好的网络索引返回    
        if(index > mWifiConfiguration.size())    
        {    
            return;    
        }    
        //连接配置好的指定ID的网络    
        mWifiManager.enableNetwork(mWifiConfiguration.get(index).networkId, true);    
    }    
    public void StartScan()    
    {    
        mWifiManager.startScan();    
        //得到扫描结果    
        mWifiList = mWifiManager.getScanResults();    
        //得到配置好的网络连接    
        mWifiConfiguration = mWifiManager.getConfiguredNetworks();    
    }    
    //得到网络列表    
    public List<ScanResult> GetWifiList()    
    {    
        return mWifiList;    
    }    
    //查看扫描结果    
    public StringBuilder LookUpScan()    
    {    
        StringBuilder stringBuilder = new StringBuilder();    
        for (int i = 0; i < mWifiList.size(); i++)    
        {    
            stringBuilder.append("Index_"+new Integer(i + 1).toString() + ":");    
            //将ScanResult信息转换成一个字符串包    
            //其中把包括:BSSID、SSID、capabilities、frequency、level    
            stringBuilder.append((mWifiList.get(i)).toString());    
            stringBuilder.append("/n");    
        }    
        return stringBuilder;    
    }    
    //得到MAC地址    
    public String GetMacAddress()    
    {    
        return (mWifiInfo == null) ? "NULL" : mWifiInfo.getMacAddress();    
    }    
    //得到接入点的BSSID    
    public String GetBSSID()    
    {    
        return (mWifiInfo == null) ? "NULL" : mWifiInfo.getBSSID();    
    }    
    //得到IP地址    
    public int GetIPAddress()    
    {    
        return (mWifiInfo == null) ? 0 : mWifiInfo.getIpAddress();    
    }    
    //得到连接的ID    
    public int GetNetworkId()    
    {    
        return (mWifiInfo == null) ? 0 : mWifiInfo.getNetworkId();    
    }    
    //得到WifiInfo的所有信息包    
    public String GetWifiInfo()    
    {    
        return (mWifiInfo == null) ? "NULL" : mWifiInfo.toString();    
    }    
    //添加一个网络并连接    
    public void AddNetwork(WifiConfiguration wcg)    
    {    
        int wcgID = mWifiManager.addNetwork(wcg);     
        mWifiManager.enableNetwork(wcgID, true);     
    }    
    //断开指定ID的网络    
    public void DisconnectWifi(int netId)    
    {    
        mWifiManager.disableNetwork(netId);    
        mWifiManager.disconnect();    
    }    
}   

 

 

Android中的PopupWindow详解

 

 

Android的对话框有两种:PopupWindow和AlertDialog。它们的不同点在于:
AlertDialog的位置固定,而PopupWindow的位置可以随意
AlertDialog是非阻塞线程的,而PopupWindow是阻塞线程的
PopupWindow的位置按照有无偏移分,可以分为偏移和无偏移两种;按照参照物的不同,可以分为相对于某个控件(Anchor锚)和相对于父控件。具体如下
showAsDropDown(View anchor):相对某个控件的位置(正左下方),无偏移
showAsDropDown(View anchor, int xoff, int yoff):相对某个控件的位置,有偏移
showAtLocation(View parent, int gravity, int x, int y):相对于父控件的位置(例如正中央Gravity.CENTER,下方Gravity.BOTTOM等),可以设置偏移或无偏移
下面通过一个Demo讲解(解释看注释):
main.xml
[html] <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >
 
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/hello" />
 
    <Button
        android:id="@+id/button01"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="以自己为Anchor,不偏移" />
 
    <Button
        android:id="@+id/button02"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="以自己为Anchor,有偏移" />
 
    <Button
        android:id="@+id/button03"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="以屏幕中心为参照,不偏移(正中间)" />
 
    <Button
        android:id="@+id/button04"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="以屏幕下方为参照,下方中间" />
 
</LinearLayout>

popup_window.xml
[html]
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="#00FF00"
    android:orientation="vertical" >
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="选择状态:"
        android:textColor="@android:color/white"
        android:textSize="20px" />
 
    <RadioGroup
        android:id="@+id/radioGroup"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical" >
 
        <RadioButton android:text="在线" />
 
        <RadioButton android:text="离线" />
 
        <RadioButton android:text="隐身" />
    </RadioGroup>
 
</LinearLayout>

PopupWindowDemoActivity.java
[java]
package com.tianjf;
 
import android.app.Activity;
import android.os.Bundle;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.PopupWindow;
import android.widget.RadioGroup;
import android.widget.RadioGroup.OnCheckedChangeListener;
 
public class PopupWindowDemoActivity extends Activity implements OnClickListener,
        OnCheckedChangeListener {
 
    private Button mbutton01;
    private Button mbutton02;
    private Button mbutton03;
    private Button mbutton04;
    private PopupWindow mPopupWindow;
    // 屏幕的width
    private int mScreenWidth;
    // 屏幕的height
    private int mScreenHeight;
    // PopupWindow的width
    private int mPopupWindowWidth;
    // PopupWindow的height
    private int mPopupWindowHeight;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
 
        mbutton01 = (Button) findViewById(R.id.button01);
        mbutton02 = (Button) findViewById(R.id.button02);
        mbutton03 = (Button) findViewById(R.id.button03);
        mbutton04 = (Button) findViewById(R.id.button04);
 
        mbutton01.setOnClickListener(this);
        mbutton02.setOnClickListener(this);
        mbutton03.setOnClickListener(this);
        mbutton04.setOnClickListener(this);
    }
 
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
        // 相对某个控件的位置(正左下方),无偏移
        case R.id.button01:
            getPopupWindowInstance();
            mPopupWindow.showAsDropDown(v);
            break;
 
        // 相对某个控件的位置(正左下方),有偏移
        case R.id.button02:
            getPopupWindowInstance();
            mPopupWindow.showAsDropDown(v, 50, 50);// X、Y方向各偏移50
            break;
 
        // 相对于父控件的位置,无偏移
        case R.id.button03:
            getPopupWindowInstance();
            mPopupWindow.showAtLocation(v, Gravity.CENTER, 0, 0);
            break;
 
        // 相对于父控件的位置,有偏移
        case R.id.button04:
            getPopupWindowInstance();
            mPopupWindow.showAtLocation(v, Gravity.BOTTOM, 0, 50);
            break;
 
        default:
            break;
        }
    }
 
    @Override
    public void onCheckedChanged(RadioGroup group, int checkedId) {
        mPopupWindow.dismiss();
    }
 
    /*
     * 获取PopupWindow实例
     */
    private void getPopupWindowInstance() {
        if (null != mPopupWindow) {
            mPopupWindow.dismiss();
            return;
        } else {
            initPopuptWindow();
        }
    }
 
    /*
     * 创建PopupWindow
     */
    private void initPopuptWindow() {
        LayoutInflater layoutInflater = LayoutInflater.from(this);
        View popupWindow = layoutInflater.inflate(R.layout.popup_window, null);
        RadioGroup radioGroup = (RadioGroup) popupWindow.findViewById(R.id.radioGroup);
        radioGroup.setOnCheckedChangeListener(this);
 
        // 创建一个PopupWindow
        // 参数1:contentView 指定PopupWindow的内容
        // 参数2:width 指定PopupWindow的width
        // 参数3:height 指定PopupWindow的height
        mPopupWindow = new PopupWindow(popupWindow, 100, 130);
 
        // 获取屏幕和PopupWindow的width和height
        mScreenWidth = getWindowManager().getDefaultDisplay().getWidth();
        mScreenWidth = getWindowManager().getDefaultDisplay().getHeight();
        mPopupWindowWidth = mPopupWindow.getWidth();
        mPopupWindowHeight = mPopupWindow.getHeight();
    }
}

 

 

Intent.ACTION_PICK 获取联系人

 加权限<uses-permission android:name="android.permission.READ_CONTACTS" />

/**
 * 获取手机联系人,并显示在页面
 * @author zhouchaoxin
 *
 */
public class MainActivity extends Activity {

 private Button button;
 private TextView textView;
 private List<HashMap<String, Object>> datas;
 String numPhone;
 String name;
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  button = (Button)findViewById(R.id.btn);
  textView = (TextView)findViewById(R.id.tfv);
  button.setOnClickListener(onClick);
 }
 /**
  * 获取联系人
  */
 private OnClickListener onClick = new OnClickListener() {
  @Override
  public void onClick(View v) {
   Intent intent = new Intent();
   //活动类型 Intent.ACTION_PICK
   intent.setAction(Intent.ACTION_PICK);
   intent.setData(android.provider.ContactsContract.Contacts.CONTENT_URI);
   startActivityForResult(intent, 110);
  }
 };
 
 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  switch (requestCode) {
  case 110:
   System.out.println(resultCode);
   Uri contactData = data.getData();
   Cursor c =  managedQuery(contactData, null, null, null, null);
   datas = new ArrayList<HashMap<String, Object>>();
   if (c.moveToFirst()) {
      //姓名
   name = c.getString(c.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
   int contactId = c.getInt(c.getColumnIndex(ContactsContract.Contacts._ID));
   
   Cursor phones = getContentResolver()
     .query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
       null,
       ContactsContract.CommonDataKinds.Phone.CONTACT_ID
         + " = " + contactId, null, null);
   int typePhone = 0;
   if (phones.getCount() > 0) {
    phones.moveToFirst();
    String[] typenumArray = new String[phones
      .getCount()];
    for (int i = 0; i < phones.getCount(); i++) {
     HashMap<String, Object> table = new HashMap<String, Object>();
     typePhone = phones
       .getInt(phones.getColumnIndex(ContactsContract.CommonDataKinds.Phone.TYPE));
     //手机号
     numPhone = phones
       .getString(phones
         .getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
     int typePhone2 = ContactsContract.CommonDataKinds.Phone
       .getTypeLabelResource(typePhone);
     String typePhone3 = getString(typePhone2);
     String typenum = typePhone3 + ":" + numPhone;
     table.put("typePhone", typePhone); // 号码所属运营商
     table.put("numPhone", numPhone); // 电话号码
     datas.add(table);
     typenumArray[i] = typenum;
     phones.moveToNext();
    }
    //创建Dialog
    AlertDialog.Builder builder = new AlertDialog.Builder(this);
    builder.setTitle("选择一个号码");
    builder.setSingleChoiceItems(typenumArray,-1, new DialogInterface.OnClickListener() {
     @Override
     public void onClick(DialogInterface dialog, int which) {
      numPhone = datas.get(which).get("numPhone").toString();
     }
    });
    builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
     @Override
     public void onClick(DialogInterface dialog, int which) {
      textView.setText(numPhone + " "+ name);
     }
     });
    builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
     @Override
     public void onClick(DialogInterface dialog, int which) {
      Intent intent = new Intent();
      intent.setAction(Intent.ACTION_PICK);
      intent.setData(android.provider.ContactsContract.Contacts.CONTENT_URI);
      startActivityForResult(intent, 110);
      }
     });
    // dialog的返回键处理
    builder.setOnKeyListener(new OnKeyListener() {
     @Override
     public boolean onKey(DialogInterface arg0,
       int arg1, KeyEvent arg2) {
      if (arg1 == KeyEvent.KEYCODE_BACK && arg2.getRepeatCount() == 0) {
       Intent intent = new Intent();
       intent.setAction(Intent.ACTION_PICK);
       intent.setData(android.provider.ContactsContract.Contacts.CONTENT_URI);
       startActivityForResult(intent, 110);
       return true;
      }
      return true;
     }
    });
    builder.create();
    builder.show();
    }
   }
    break;
  }
 };
}
 

 

Android获取屏幕宽、高、位深密度(density)

DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);

Log.d(TAG, "metrics: " + metrics.toString());
Log.d(TAG, "metrics: " + metrics.density + ", width: " + metrics.widthPixels + ", height: " + metrics.heightPixels);
位深密度(density)

density值表示每英寸有多少个显示点,与分辨率是两个不同的概念。

Android主要有以下几种屏:

QVGA和WQVGA屏density=120;

HVGA屏density=160;

WVGA屏density=240;

下面以480dip*800dip的WVGA(density=240)为例,详细列出不同density下屏幕分辨率信息:

当density=120时 屏幕实际分辨率为240px*400px (两个点对应一个分辨率)
状态栏和标题栏高各19px或者25dip
横屏是屏幕宽度400px 或者800dip,工作区域高度211px或者480dip
竖屏时屏幕宽度240px或者480dip,工作区域高度381px或者775dip

density=160时 屏幕实际分辨率为320px*533px (3个点对应两个分辨率)
状态栏和标题栏高个25px或者25dip
横屏是屏幕宽度533px 或者800dip,工作区域高度295px或者480dip
竖屏时屏幕宽度320px或者480dip,工作区域高度508px或者775dip

density=240时 屏幕实际分辨率为480px*800px (一个点对于一个分辨率)
状态栏和标题栏高个38px或者25dip
横屏是屏幕宽度800px 或者800dip,工作区域高度442px或者480dip
竖屏时屏幕宽度480px或者480dip,工作区域高度762px或者775dip

apk的资源包中,当屏幕density=240时使用hdpi标签的资源
当屏幕density=160时,使用mdpi标签的资源
当屏幕density=120时,使用ldpi标签的资源。
不加任何标签的资源是各种分辨率情况下共用的。
建议:布局时尽量使用单位dip,少使用px。

device independent pixels(设备独立像素). 不同设备有不同的显示效果,这个和设备硬件有关,一般我们为了支持WVGA、HVGA和QVGA 推荐使用这个,不依赖像素。

 

 

  1. import android.content.Context;  
  2. import android.util.DisplayMetrics;  
  3.   
  4. /** 
  5.  * 计算公式 pixels = dips * (density / 160) 
  6.  *  
  7.  * @version 1.0.1 2010-12-11 
  8.  *  
  9.  * @author 
  10.  */  
  11. public class DensityUtil {  
  12.       
  13.     private static final String TAG = DensityUtil.class.getSimpleName();  
  14.       
  15.     // 当前屏幕的densityDpi  
  16.     private static float dmDensityDpi = 0.0f;  
  17.     private static DisplayMetrics dm;  
  18.     private static float scale = 0.0f;  
  19.   
  20.     /** 
  21.      *  
  22.      * 根据构造函数获得当前手机的屏幕系数 
  23.      *  
  24.      * */  
  25.     public DensityUtil(Context context) {  
  26.         // 获取当前屏幕  
  27.         dm = new DisplayMetrics();  
  28.         dm = context.getApplicationContext().getResources().getDisplayMetrics();  
  29.         // 设置DensityDpi  
  30.         setDmDensityDpi(dm.densityDpi);  
  31.         // 密度因子  
  32.         scale = getDmDensityDpi() / 160;  
  33.         Logger.i(TAG, toString());  
  34.     }  
  35.   
  36.     /** 
  37.      * 当前屏幕的density因子 
  38.      *  
  39.      * @param DmDensity 
  40.      * @retrun DmDensity Getter 
  41.      * */  
  42.     public static float getDmDensityDpi() {  
  43.         return dmDensityDpi;  
  44.     }  
  45.   
  46.     /** 
  47.      * 当前屏幕的density因子 
  48.      *  
  49.      * @param DmDensity 
  50.      * @retrun DmDensity Setter 
  51.      * */  
  52.     public static void setDmDensityDpi(float dmDensityDpi) {  
  53.         DensityUtil.dmDensityDpi = dmDensityDpi;  
  54.     }  
  55.   
  56.     /** 
  57.      * 密度转换像素 
  58.      * */  
  59.     public static int dip2px(float dipValue) {  
  60.   
  61.         return (int) (dipValue * scale + 0.5f);  
  62.   
  63.     }  
  64.   
  65.     /** 
  66.      * 像素转换密度 
  67.      * */  
  68.     public int px2dip(float pxValue) {  
  69.         return (int) (pxValue / scale + 0.5f);  
  70.     }  
  71.   
  72.     @Override  
  73.     public String toString() {  
  74.         return " dmDensityDpi:" + dmDensityDpi;  
  75.     }  
  76. }  

 

/**
     * 根据手机的分辨率从 dp 的单位 转成为 px(像素)
     */ 
    public static int dip2px(Context context, float dpValue) { 
        final float scale = context.getResources().getDisplayMetrics().density; 
        return (int) (dpValue * scale + 0.5f); 
    }
   
    /**
     * 根据手机的分辨率从 px(像素) 的单位 转成为 dp
     */ 
    public static int px2dip(Context context, float pxValue) { 
        final float scale = context.getResources().getDisplayMetrics().density; 
        return (int) (pxValue / scale + 0.5f); 
    }

 

基础

Android中更新UI处理必须由创建它的线程更新,而不能在其他线程中更新,如果需要在子线程中更新UI,则使用Handler、Message来发送消息实现子线程和主线程通信。

 

Application context和Activity context的区别

 

这是两种不同的context,也是最常见的两种.第一种中context的生命周期与Application的生命周期相关的,context随着Application的销毁而销毁,伴随application的一生,与activity的生命周期无关.第二种中的context跟Activity的生命周期是相关的,但是对一个Application来说,Activity可以销毁几次,那么属于Activity的context就会销毁多次.至于用哪种context,得看应用场景,个人感觉用Activity的context好一点,不过也有的时候必须使用Application的context.application context可以通过

Context.getApplicationContext或者Activity.getApplication方法获取.
还有就是,在使用context的时候,小心内存泄露,防止内存泄露,注意一下几个方面:
 1. 不要让生命周期长的对象引用activity context,即保证引用activity的对象要与activity本身生命周期是一样的
 2. 对于生命周期长的对象,可以使用application context
 3. 避免非静态的内部类,尽量使用静态类,避免生命周期问题,注意内部类对外部对象引用导致的生命周期变化
 
现在回到正题,说一下android全局变量,在平时的开发过程中,有时候可能会需要一些全局数据,来让应用中的所有Activity和View都能访问到,大家在遇到这种情况时,可能首先会想到自己定义一个类,然后创建很多静态成员,android已经为我们提供了这种情况的解决方案:
在Android中,有一个Application类,在Activity中可以使用getApplication()方法获得实例,使用它就可以获得当前应用的主题、资源文件中的内容等,这个类更灵活的一个特性就是可以被继承,来添加自己的全局属性.例如开发一个游戏,需要保存分数,那么我们就可以继承Application
下面是个demo,用的是http://www.eoeandroid.com/thread-30257-1-1.html上面的代码,其实上面那段话也是对它的修改.
首先,先写个Application的子类:
  1. import android.app.Application;
  2. public class GameApplication extends Application {
  3. private int score;
  4. public int getScore() {
  5. return score;
  6. }
  7. public void setScore(int score) {
  8. this.score = score;
  9. }
  10. }
  11. //然后在manifest.xml文件里面修改:
  12. <application android:name=".GameApplication" android:icon="@drawable/icon" android:label="@string/app_name">
  13. <activity android:name=".DemoActivity"
  14. android:label="@string/app_name">
  15. <intent-filter>
  16. <action android:name="android.intent.action.MAIN" />
  17. <category android:name="android.intent.category.LAUNCHER" />
  18. </intent-filter>
  19. </activity>
  20. <activity android:name="ResultActivity">
  21. </activity>
  22. </application>
  23. //注意到添加了android:name=".GameApplication" .  
  24. 修改完了以后,再往下看:
  25. public class DemoActivity extends Activity {
  26. public Button button;
  27. @Override
  28. public void onCreate(Bundle savedInstanceState) {
  29. super.onCreate(savedInstanceState);
  30. setContentView(R.layout.main);
  31. button=(Button)findViewById(R.id.button);
  32. ((GameApplication)getApplication()).setScore(100);
  33. button.setOnClickListener(new View.OnClickListener() {
  34. public void onClick(View v) {
  35. Intent intent=new Intent();
  36. intent.setClass(DemoActivity.this, ResultActivity.class);
  37. startActivity(intent);
  38. }
  39. });
  40. }
  41. }
复制代码
在这个activity里面设置了分数,我们可以在别的activity里面取出来:
  1. public class ResultActivity extends Activity {
  2. @Override
  3.  protected void onCreate(Bundle savedInstanceState) {
  4. // TODO Auto-generated method stub
  5. super.onCreate(savedInstanceState);
  6. setContentView(R.layout.other);
  7. TextView tv=(TextView)findViewById(R.id.tv);
  8. int score=((GameApplication)getApplicationContext()).getScore();
  9.  tv.setText("你的成绩是:"+score);
  10.  }
  11. }
复制代码
这只是个简单的例子,当然,想要完成以上功能,使用intent传值就可以了,这样还显得麻烦,但是,如果有很多activity,使用这种方法就会发现很有用,是不是使用sharepreference也可以完成类似功能呢,可以,但是,效率方面就要比这个差很多了,sharepreference主要是用来存储数据,你可以退出程序时把所需要保存的简单数据保存到sharepreference里面,当然复杂的数据,还得使用sqllite.
 
 横竖屏切换时候Activity的生命周期。  这个生命周期跟清单文件里的配置有关系 
1、不设置Activity的android:configChanges时,切屏会重新调用各个生命周期 默认首先销毁当前activity,然后重新加载 
Onpause onstop ondestory oncreate onstart onresume
 

2

、设置

Activity

android:configChanges="orientation|keyboardHidden"

时,切屏不

会重新调用各个生命周期,只会执行

onConfigurationChanged

方法

2、设置Activity的android:configChanges="orientation|keyboardHidden"时,切屏不会重新调用各个生命周期,只会执行onConfigurationChanged方法
分享到:
评论

相关推荐

    initrd.gz.ubuntu9.10

    硬盘安装ubuntu9.10所需。 注:如果下载文件被CSDN改名,请自行修改为"initrd.gz"。

    MNIST训练数据和验证数据,gz格式,不是mat格式哟

    tensorflow学习的入门基础,往往都是参照MNIST进行学习的,这里就涉及到手写图片数字的识别问题。本资源就是相关的图片资源: TRAIN_IMAGES = 'train-images-idx3-ubyte.gz' TRAIN_LABELS = 'train-labels-idx1-...

    nessus-updates-8.11.0.tar.gz

    nessus-update-8.11.0.tar.gz 更新时间为2020.07.14 亲测有效,如有问题可以一起讨论。

    navicat121_premium_cs_x64.tar.gz

    3.界面中文乱码问题:vi /opt/navicat121_premium_cs_x64/start_navicat,将export LANG=" "改为export LANG="zh_CN.UTF-8"即可解决中文乱码问题 4.添加快捷方式:sudo gedit /usr/share/applications/navicat....

    kustomize 的安装包kustomize-v4.5.7-linux-amd64.tar.gz

    https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize/v4.5.7/kustomize_v4.5.7_linux_amd64.tar.gz 问题解决

    ftdi_sio-1.3.3.tar.gz

    ftdi_sio-1.3.3.tar.gz 关于it的问题

    navicat120_mysql_en_x64.tar.gz

    tar zxvf navicat112_mysql_cs_x64.tar.gz 进入解压的目录,运行如下命令 ./start_navicat 问题一:中文乱码 解决:打开start_navicat文件 将export LANG="en_US.UTF-8"改为export LANG="zh_CN.UTF-8" 问题二:试用...

    apache-tomcat-8.5.23.tar.gz

    apache-tomcat-8.5.23.tar.gz 我的Win 8 系统,就是用的这个版本 没有问题

    mnist.pkl.gz

    请放心下载,本人代码亲测有效!请解压后使用。因为本人正在学习神经网络,所以需要用这个数据集进行神经网络的调试,亲测有效。如有问题,请及时联系本人!

    esp_install_tar.tar.gz (包含8个压缩包)

    esp install.sh安装环境,压缩包比较难下,分享给大家,我自己测试是没问题的,esp-idf 4.0和4.2版本都是下面的压缩包,都可以intall环境和编译 ./dist/xtensa-esp32-elf-gcc8_2_0-esp-2020r2-linux-amd64.tar.gz ./...

    e1000e-3.5.1.tar.gz

    在监测任务中检查 PCIm 功能状态和执行 PHY 重置后,解决了这一问题。 旨在 该驱动程序包括对基于英特尔®安腾®2的支持,以及英特尔® EM64T 系统。此版本支持最新的2.4 系列内核以及2.6、x.x.x.x 和版本。 ...

    cronolog-1.6.2.tar.gz cronolog-1.6.2下载

    cronolog-1.6.2下载。日志轮询工具cronolog,推荐设置按天轮询或按小时轮询,解决应用日志过大问题。(文件全称:cronolog-1.6.2.tar.gz)

    redis-4.0.1.tar.gz和tcl8.6.1-src.tar.gz安装包

    redis-4.0.1.tar.gz和tcl8.6.1-src.tar.gz安装包自己亲自安装过没问题可以参考 http://blog.csdn.net/xiangyuanhong08/article/details/77884644进行安装

    nacos-server-1.1.3.tar.gz

    nacos-server-1.1.3.tar.gz,github上nacos的tar.gz资源,解决Github上下载速度奇慢问题。

    VMware-Tools-core-10.3.21-14772444.tar.gz

    最新版本的VMtools,解决Ubuntu共享文件无法查看问题。 复制CD中的VMwareTools-10.3.21-14772444.tar.gz文件到根目录下: cp /media/user/VMware\ Tools/VMwareTools-10.3.21-14772444.tar.gz / 然到移动到...

    rlwrap-0.37.tar.gz

    解决sqlplus连接oracle出现乱码问题

    zeromq-4.1.8.tar.gz

    zeromq-4.1.8.tar.gz 有问题请联系

    redis-5.0.4.tar.gz下载及redis安装过程

    注:易碰到的问题,时间错误. 原因: 源码是官方configure过的,但官方configure时,生成的文件有时间戳信息, Make只能发生在configure之后, 如果你的虚拟机的时间不对,比如说是2012年 解决: date -s ' yyyy-mm-...

    VMwareTools-10.0.5-3228253.tar.gz

    linux版本VMwareTools-10.0.5-3228253.tar.gz。 解决无法使用wmare虚拟机直接安装的问题 使用方法:下载-&gt;解压-&gt;cd vmware-tools-distrib-&gt; ./wmare-install.pl 安装过程“全程选择默认配置 即 回车”

    kibana-6.2.4-linux-x86_64.tar.gz

    kibana-6.2.4-linux-x86_64.tar.gz elk 组件之一开源实时日志分析ELK平台能够完美的解决我们上述的问题,ELK由ElasticSearch、Logstash和Kiabana三个开源工具组成

Global site tag (gtag.js) - Google Analytics