Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all 5930 articles
Browse latest View live

untiy 简单的shader应用

$
0
0

   今天和大家分享一下一个shader的效果。先分享一波图看看我们这个效果该怎么写。


这个效果就是我们今天要做的一个shader效果,其实他是个水波效果,当我们在池塘旁边,往池塘旁边扔进去一个石头,我们看池塘底的东西的时候,我们发现池底的东西是发生了弯曲的,所以我们这个效果就是来模仿现实生活中的这种简单的物理现象。我以前在gamejam上看到一款叫做鲤的游戏。当我看见他们做的那个水波的时候 我就呵呵了,只能说一般吧。那个游戏的难度很低,除了一个引导力行为的移动外没有别的了。如果把这种效果用上的话,我感觉就比较接近生活中的鲤鱼在水中游的情景了。步入正题吧,开始讲一下这个shader的原理吧。这种shader的效果是作用于摄像机的,所以它的渲染层比一般场景中物体的层要高但是比ui要低一些。所以我们还是需要借助

OnRenderImage(RenderTexture source,RenderTexture destination)这个函数来渲染一张图片到摄像机屏幕上。首先我们以屏幕的宽度和高度的一半为中心点。

转换为视口坐标的话(0.5,0.5)。然后根据时间的推移来计算水波推移的位置及推移位置左右的uv偏移。有了这个思路之后然后的shader就应该好写了。首先定义一个基本的shader格式:

Shader "Custom/Default" {
	Properties 
	{
	    //属性的添加。	
	}
	CGINCLUDE
	#include "UnityCG.cginc"
        //定义一些变量及顶点函数和片段函数。			
	ENDCG
	SubShader
	{
	  Pass
	  {
	     ZTest Always Cull Off ZWrite Off
		 CGPROGRAM
                 //这里基本的定义顶点和片段函数了
		 #pragma vertex vert_img
		 #pragma fragment frag
		 ENDCG
	  }
	}		
}
然后定义最主要的一个函数。同时将属性和变量一起定义了。

        Properties 
	{
		_MainTex ("MainTex", 2D) = "white" {}
		_GradTex("GradTex",2D) = "white" {}
		_DefaultColor("Default_Color",Color)=(0,0,0,0)

		_Param1("Param",float)=(0,0,0,0)//一个vector4变量 分别代表水波的中心点(x,y),还有水波速度倒数
		_Param2("Param",float)=(0,0,0,0)//一个vector4变量 分别代表屏幕宽度(x方向)和长度的比例(aspect,1),水波的反射率和折射率
		_Param3("Param",float)=(0,0,0,0)//一个vector4变量 分别代表屏幕长度(y方向)和宽度的比例(aspect,1),水波移动了多长时间
		
	}
	CGINCLUDE
	#include "UnityCG.cginc"

	sampler2D _MainTex;
	float2 _MainTex_TexelSize;

	half4 _DefaultColor;
	sampler2D _GradTex;

	half4 _Param1;
	half4 _Param2;
	half4 _Param3;



	float Wave(float2 pos,float2 center,float time)
	{ 
	   float dis = length(pos-center);
	   float t = time - dis * _Param1.z;
	   return (tex2D(_GradTex,float2(t,0)).a-0.5) * 2;
	}

	half4 frag(v2f_img  i):SV_Target
	{
	     const float2 dx=float2(0.01,0);
	     const float2 dy=float2(0,0.01);
	     float2 pos=i.uv*_Param2.xy;
	     float w=Wave(pos,_Param1.xy,_Param3.z);

	     float x = Wave(pos+dx,_Param1.xy,_Param3.z);
	     float y = Wave(pos+dy,_Param1.xy,_Param3.z);
	     float2 dw = float2(x-w,y-w);
	     float2 delta = dw * _Param3.xy * 0.1 * _Param2.z;
	     half4 c = tex2D(_MainTex, i.uv + delta);

	     float lerpparam = pow(length(dw) * 3 * _Param2.w, 5);
	     half4 endvalue=lerp(c,_DefaultColor,lerpparam);

	     return endvalue;
	}
	ENDCG

首先先讲一下Wave函数。Wave函数包含3个参数,分别为水波的位置(范围为0到1),当前点uv点的信息。水波偏移的时间。首先计算2个点的位置之间的距离,然后距离除以乘以速度的倒数就得到水波偏移到这个点需要的时间了。然后通过当前水波已经移动的时间和到这个点的时间差,如果当到0.5s的时候,而到达我们最边界点可能需要的时间是大于0.5的话,那么这个点的uv信息应该是不应该发生变化的,所以这个这里返回一个0了,同样这里采用是a-0.5然后在乘以2这种方法来控制alpha的范围在0和1之间。我们这里讲距离统一转换为时间的长短了。如果我们水波运行的时间为0.5,现在0.4距离的点应该水波应该是水波uv的偏移应该还是蛮大的,这时在0的点位置的uv偏移应该很小了(大家意淫一下,在脑海中想象一下,大家应该可以理解吧。),最后来说说最重要的片段着色函数了。首先定义2个偏移,一个在x轴和一个y轴的偏移。然后计算一下出一个该点总的uv偏移应该是多少。 half4 c = tex2D(_MainTex, i.uv + delta);delta就算该点uv的偏移量,这个也好理解,我们同样也可以让该点的uv向y轴发生偏移而不向x轴偏移。同样也可以让x轴偏移是y轴偏移的2倍,这个大家可以试试,看看效果会是怎样的。具体的代码是上面的dx和dy。最后就是一个颜色的插值了。首先我们求出插值所需的2个区间和一个t参数。最后我们就在通过先建一个材质来运用它,同样还需要定义我们的水波衰减的曲线就是如果水波到0.5的时候0.4的点水波应该衰减多少(这里我们通过一张贴图的alpha来模拟,我们也可以通过一个曲线来模拟,不过曲线的区间我们需要收缩到[0,1]区间),0.2的点水波应该衰减多少。所以我们仍需要一个脚本来控制这个。

public class SampleRippleEffect : MonoBehaviour
{

    public AnimationCurve Waveform = new AnimationCurve(
       new Keyframe(0.00f, 0.50f, 0, 0),
       new Keyframe(0.05f, 1.00f, 0, 0),
       new Keyframe(0.15f, 0.10f, 0, 0),
       new Keyframe(0.25f, 0.80f, 0, 0),
       new Keyframe(0.35f, 0.30f, 0, 0),
       new Keyframe(0.45f, 0.60f, 0, 0),
       new Keyframe(0.55f, 0.40f, 0, 0),
       new Keyframe(0.65f, 0.55f, 0, 0),
       new Keyframe(0.75f, 0.46f, 0, 0),
       new Keyframe(0.85f, 0.52f, 0, 0),
       new Keyframe(0.99f, 0.50f, 0, 0)
   ); 
}
上面就定义了一条衰减曲线了。接下来添加我们最重要的unity自带的api了。
  private void OnRenderImage(RenderTexture source,RenderTexture destination)
    {
        Graphics.Blit(source, destination, _material);
    }
然后紧接着定义材质,shader及shader的一些参数,我就直接贴出来了。
      new Keyframe(0.99f, 0.50f, 0, 0)
   );

    <span style="background-color: rgb(255, 102, 0);">[SerializeField] private Color _defaultColor;
    [SerializeField] private Shader _shader;
    [SerializeField,Range(1,5)] private float _moveSpeed;
    [SerializeField, Range(0, 1)] private float _reflection;
    [SerializeField, Range(0, 1)] private float _refraction;

    private Vector4 _param1;
    private Vector4 _param2;
    private Vector4 _param3;

   
    private float _aspect;
    private Material _material;
    private Texture2D _gradTexture;</span>
添加基本的start方法。功能是构造出我们的曲线。这里我们将曲线表现用一个贴图的alpha来表现(其实完全可以用曲线来表现,但是需要保存的数据非常多了)

 private void Start()
    {
        _totalTime = 0;
        _aspect = GetComponent<Camera>().aspect;
        _gradTexture = new Texture2D(2048, 1, TextureFormat.Alpha8, false);
        _gradTexture.wrapMode = TextureWrapMode.Clamp;
        _gradTexture.filterMode = FilterMode.Bilinear;
        for (var i = 0; i < _gradTexture.width; i++)
        {
            var x = 1.0f / _gradTexture.width * i;
            var a = Waveform.Evaluate(x);
            _gradTexture.SetPixel(i, 0, new Color(a, a, a, a));
        }
        _gradTexture.Apply();

        _material = new Material(_shader);
        _material.hideFlags = HideFlags.DontSave;
        _material.SetTexture("_GradTex", _gradTexture);
    }
接下来就是添加update方法来每帧刷新水波的移动的位置。
   private void UpdateShaderParams(Vector2 mouseVector2,float time)
    {
        _param1 = new Vector4(mouseVector2.x,mouseVector2.y,1/_moveSpeed);
        _param2 = new Vector4(_aspect, 1, _reflection, _refraction);
        _param3 = new Vector4(1,1/_aspect,time,0);

        _material.SetColor("_DefaultColor", _defaultColor);
        _material.SetVector("_Param1", _param1);
        _material.SetVector("_Param2", _param2);
        _material.SetVector("_Param3", _param3);
    }

    private Vector2 _centerVector2;
    private float _totalTime;
    private void Update()
    {
        _totalTime += Time.deltaTime;
        if (Input.GetMouseButtonDown(0)&& _totalTime>1)
        {
            _centerVector2 = Camera.main.ScreenToViewportPoint(Input.mousePosition);
            _totalTime = 0;
        }
        UpdateShaderParams(_centerVector2, _totalTime);
    }
UpdateShaderParams主要是对shader中的参数进行重新赋值操作(shader中存在分OpGL和Dx渲染下 uv的y的起始位置不同,所以根据不同的平台对uv进行定义)

好了写完,如果有不懂可以qq私聊我。qq:1850761495.欢迎各位骚扰。




作者:u012565990 发表于2016/8/14 23:49:22 原文链接
阅读:214 评论:0 查看评论

Anroid沉浸式状态栏

$
0
0

作者:Cyning
首发地:Cyning的博客

概要

Metarial Design是2014年Google IO的一个重点,在过去的两年时光里,越来越多的公司已经开始认可MD设计规范。在dribbble上可以越来越多的设计师开始投入到MD设计实践中,MD设计规范终于有底气可以和IOS的设计规范对抗啦Android程序员可以很叫嚣滴告诉设计师这就是Android的设计规范。

很少写Material Design的东西,今天趁着手热,在Material化财经APP的时候,看了些透明状态栏/沉浸式状态栏的东西,觉得自己可能还有很多不足之处只是希望能分享出来,一方面是自己的学习成果,另一方面是希望大家指正自己在理解的不到位或者错误。

先放一张我的五儿子手机原生的短信截图:

图1

从上到下,一次是状态栏,内容View,导航栏(有些机器上不一定有导航栏这个虚拟栏),可以看到状态栏是彩色的,不是以前那种黑乎乎的状态栏,现在你也可以定制自己的状态栏了。

现在我们就来看下我手机上装的APP的一些截图:

图2

这个五个应用分别是网易新闻、豌豆荚、微信、小米天气、小气天气。

  关于这个状态栏变色到底叫「Immersive Mode」/「Translucent Bars」有兴趣可以去 为什么在国内会有很多用户把 「透明栏」(Translucent Bars)称作 「沉浸式顶栏」?以及何为沉浸模式,沉浸式顶栏,变色龙状态栏,这个历史原因我们就没法去说大家错误,所有就错说错有了沉浸式状态栏一说。

在5.0之后我们是可以通过v7包下的Theme.AppCompat一些列主题为APP的页面设置Activity的的状态栏,但是为了兼容低版本的我们放弃这种方式。为了能定制自己的状态栏,我们可以手动设置状态栏的颜色,可以随时改变状态栏的透明度等,所以我们需要自己来定制一套可行的方案。

实现

准备

分类

  1. 全屏模式和着色模式
    根据内容延伸的角度可以分为两类:全屏模式和着色模式。其中,图2 中前三个页面都是状态栏固定在上方,无论下面怎么滑动,内部View都是那一块固定的大小区域滑动,由于状态栏是固定在一定位置且有着色,我们称之为着色模式,而图2中后两个页面截图中,内容View和状态栏像放在一个FrameLayout一样,是层叠关系并且状态栏是透明,内容View可以延伸到状态栏,我们称之为全屏模式。

  2. 透明状态栏和彩色状态栏
    根据状态栏的颜色,可以分为状态栏透明和不透明(网易新闻、小米天气都是透明,而豌豆荚、wechat都是不透明)。
    以上两种分为是不同维度的,从不同维度来看可以组合如下图:

setFitsSystemWindows

在android doc是这么描述的:

void setFitsSystemWindows (boolean fitSystemWindows)
Sets whether or not this view should account for system screen decorations such as the status bar and inset its content; that is, controlling whether the default implementation of fitSystemWindows(Rect) will be executed. See that method for more details.

Note that if you are providing your own implementation of fitSystemWindows(Rect), then there is no need to set this flag to true – your implementation will be overriding the default implementation that checks this flag.

大概意思是:setFitsSystemWindows用来设置影响系统的工具栏如状态栏,决定了这个view是否插入到它的ContentView。当您设置了fitSystemWindows(Rect)而没有将setFitsSystemWindows设置为true,你对fitSystemWindows(Rect)设置是无效的.

该属性可以设置是否为系统 View 预留出空间, 当设置为 true 时,会预留出状态栏的空间。

由于这个实在API14(4.0)之后的函数,为了兼容低版本,才有V4里面的

 ViewCompat.setFitsSystemWindows(rootView,false);

透明状态栏

由于Android 4.4才加入透明状态栏,Android 5.0之后可以直接设置状态栏和导航栏,而不是之前的黑乎乎的状态栏,但是,在4.4以下的系统上,想自己定义状态栏就无能为力了。所以,我们将Android 4.4和Android5.0视为边界。

Android 4.4设置透明状态栏

设置方法有两种:

代码

 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            // 设置状态栏透明
             activity.getWindow()
                .setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);

}

XML

在values-v19文件夹下,为activity的Style 添加一个属性:

“`xml

<style name="AppTheme" parent="@style/BaseAppTheme">
    <item name="android:windowTranslucentStatus">true</item>
</style>


“`

Android 5.0+透明状态栏

不过对于5.0系统,上面的设置后的结果可能不是透明哦(在原生机器是不透明的,但是在小米的是透明的,估计MIUI做了一些优化工作)

若是要完全透明,就需要看额外处理,在内容延伸到状态栏一节有介绍。

当然了你也可以用5.0的setStatusBarColor

全屏模式的透明状态栏

Window window = activity.getWindow();
//设置透明状态栏,这样才能让 ContentView 向上
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); 

//需要设置这个 flag 才能调用 setStatusBarColor 来设置状态栏颜色
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); 
//设置状态栏颜色
window.setStatusBarColor(statusColor);

//为了设置全屏
ViewGroup mContentView = (ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT);
View mChildView = mContentView.getChildAt(0);
if (mChildView != null) {
    //注意不是设置 ContentView 的 FitsSystemWindows, 而是设置 ContentView 的第一个子 View . 使其不为系统 View 预留空间.
    ViewCompat.setFitsSystemWindows(mChildView, false);

}

ViewCompat.setFitsSystemWindows(mChildView, false)中的第二个参数设置为false就是全屏模式,而设置成true。像上述实例中, ViewCompat.setFitsSystemWindows(mChildView, false)就是说mChildView可以直接延伸到phoneWindow的顶部,相当于小米天气的那种效果。

彩色状态栏

有透明状态栏,就有彩色状态栏。
在5.0+设置状态栏很简单就参照全屏模式的透明状态栏中的代码做修改window.setStatusBarColor(int color),而对于Android 4.4–5.0的怎么办呢,Android4.4是提供setStatusBarColor这个方法的。

我们就想到了在上面提到的全屏透明状态栏的基础上加一个和状态栏一样高度的空白View放到顶部。

好了说干就干:

        activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
 //添加一个空白的view到手机屏幕的顶部
addStatusBarBehind(activity, color, statusBarAlpha);
public static void addStatusBarBehind(Activity activity, int color, int statusBarAlpha) {
        ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
        int       count     = decorView.getChildCount();
        if (count > 0 && decorView.getChildAt(count - 1) instanceof StatusBarView) {
            decorView.getChildAt(count - 1).setBackgroundColor(calculateStatusColor(color, statusBarAlpha));
        } else {
            StatusBarView statusView = createStatusBarView(activity, color, statusBarAlpha);
            decorView.addView(statusView);
        }
        setRootView(activity);
    }
 ```



## 全屏模式


全屏模式,内容延伸到状态栏类似与小米天气的APP,先考虑下怎么做?

>答案:将内容移到状态栏下,并且状态栏背景透明.

### Android4.4--5.0

```java
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            // 设置状态栏透明
            activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
            //  设置根布局的参数
            ViewGroup rootView = (ViewGroup) ((ViewGroup) activity.findViewById(android.R.id.content)).getChildAt(0);
            ViewCompat.setFitsSystemWindows(rootView,false);
            rootView.setClipToPadding(true);
        }




<div class="se-preview-section-delimiter"></div>

其实就先将状态栏设置了透明activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS),而后可以直接设置内容的根View来直接设置ViewCompat.setFitsSystemWindows(rootView,false),这样就可以直接讲根rootView直接顶上去,和状态栏的顶部对齐。

Android 5.0+

在Android 4.4上设置透明状态栏,在5.0上依然可以正常显示,利用5.0实现的全屏模式的透明状态栏不过状态栏实际上并不完全为透明色,会有些许灰色。一般情况下,这个使我们能接受的,如微信、QQ等都是状态栏颜色暗与下面的Toolbar的。

若是能通过Activity的Theme的colorPrimaryDark设状态栏颜色颜色设置,也是可行的,但是若你must实现透明额状态栏,也只能出狠招了。

Window window = activity.getWindow(); 

window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS|
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); 
window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);           window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(Color.TRANSPARENT);    

setSystemUiVisibility这个可以参考Using Immersive Full-Screen Mode


这样的组合,让我们可以按照自己的需求来定制自己的状态栏,随后会开源自己封装的开源库工供大家参考。

参考文章

  1. Using Immersive Full-Screen Mode

  2. Android实现沉浸式状态栏

  3. Android-transulcent-status-bar

  4. Android状态栏合集-管你透不透明

作者:ownWell 发表于2016/8/14 23:50:16 原文链接
阅读:233 评论:0 查看评论

Android 联系人数据库介绍以及对联系人的基本操作

$
0
0

一、 联系人数据库
联系人的数据库文件的位置
/data/data/com.Android.providers.contacts/databases.contacts2.db
这里写图片描述

数据库中重要的几张表
这里写图片描述
1、contacts表
该表保存了所有的手机测联系人,每个联系人占一行,该表保存了联系人的
ContactID、联系次数、最后一次联系的时间、是否含有号码、是否被添加
到收藏夹等信息。
这里写图片描述
2、raw_contacts表
该表保存了所有创建过的手机测联系人,每个联系人占一行,表里有一列标
识该联系人是否被删除,该表保存了两个ID:RawContactID和ContactID,
从而将contacts表和raw_contacts表联系起来。该表保存了联系人的
RawContactID、ContactID、联系次数、最后一次联系的时间、是否被添
加到收藏夹、显示的名字、用于排序的汉语拼音等信息。
这里写图片描述

3、 mimetypes
该表定义了所有的MimeTypeID,即联系人的各个字段的唯一标志。
这里写图片描述

4、data表
该表保存了所有创建过的手机测联系人的所有信息,每个字段占一行 ,该表
保存了两个ID:MimeTypeID和RawContactID,从而将data表和
raw_contacts表联系起来。
联系人的所有信息保存在列data1至data15中,各列中保存的内容根据
MimeTypeID的不同而不同。如保存号码(MimeTypeID=5)的那行数据中,
data1列保存号码,data2列保存号码类型(手机号码/家庭号码/工作号码等)。
这里写图片描述

一:联系人数据库查询

public class MainActivity extends Activity {

    private ListView listView;
    private TextView empty;

    private ContentResolver resolver;

//  联系人表的uri
    private Uri contactsUri = Contacts.CONTENT_URI;

    private Uri phoneUri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;

    private Uri emailUri = ContactsContract.CommonDataKinds.Email.CONTENT_URI;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d("flag","------------->NUMBER: "+ContactsContract.CommonDataKinds.Phone.NUMBER);

        initView();

        resolver = getContentResolver();

        Cursor contactsCursor = resolver.query(contactsUri, null, null, null, null);

        List<Map<String,Object>> data = parseCursor(contactsCursor);

        SimpleAdapter adapter = new SimpleAdapter(this, data, R.layout.item, 
                new String[]{"display_name","phone","email"}, new int[]{R.id.displayName,R.id.phone,R.id.email});

        listView.setAdapter(adapter);

        listView.setEmptyView(empty);

    }


    private List<Map<String, Object>> parseCursor(Cursor contactsCursor) {
        List<Map<String,Object>> data = new ArrayList<Map<String,Object>>();
        while(contactsCursor.moveToNext()){
            Map<String,Object> map = new HashMap<String, Object>();

//          获取联系人的id
            int _id = contactsCursor.getInt(contactsCursor.getColumnIndex(Contacts._ID));

            String name = contactsCursor.getString(contactsCursor.getColumnIndex(Contacts.DISPLAY_NAME));

            map.put("display_name", name);

//          根据联系人的_id(此处获取的_id和raw_contact_id相同,因为contacts表就是通过这两个字段和raw_contacts表取得联系)查询电话和email

//          查询电话
            Cursor phoneCursor = resolver.query(phoneUri, new String[]{ContactsContract.CommonDataKinds.Phone.DATA1}, "raw_contact_id = ?", new String[]{_id+""}, null);
            String phone = "";
            while(phoneCursor.moveToNext()){
                phone = phoneCursor.getString(phoneCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
            }
            map.put("phone", phone);
            phoneCursor.close();
            phoneCursor = null;

//          查询邮箱
            Cursor emailCursor = resolver.query(emailUri, new String[]{ContactsContract.CommonDataKinds.Email.ADDRESS}, "raw_contact_id = ?", new String[]{_id+""}, null);
            String email = null;
            while(emailCursor.moveToNext()){
                email = emailCursor.getString(emailCursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS));
            }
            map.put("email", email);

            data.add(map);
        }

        return data;
    }


    private void initView() {
        // TODO Auto-generated method stub
        listView = (ListView) findViewById(R.id.listView);
        empty = (TextView) findViewById(R.id.empty);

    }
}

二:联系人数据库添加

//  插入数据
    public void btnInsert(View view){
//      1、向rawUri中插入一条空的数据
        ContentValues values = new ContentValues();

        Uri insert = resolver.insert(RawContacts.CONTENT_URI, values);

//      2、获取raw_id
        long _id= ContentUris.parseId(insert);

//      3、向data表中添加数据,向data表中插入数据是分条添加,因为调用一次insert只能向数据库中添加一个字段的数据
        ContentValues valuesName = new ContentValues();
//      3.1、向哪一条联系人中添加数据
        valuesName.put(ContactsContract.Data.RAW_CONTACT_ID, _id);

//      3.2、添加名字
//      data1
        valuesName.put(ContactsContract.CommonDataKinds.StructuredName.DATA1, "huanhuanlele");
//      3.3、指定类型
        valuesName.put(ContactsContract.Data.MIMETYPE, 
                ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
        resolver.insert(ContactsContract.Data.CONTENT_URI, valuesName);


//      添加电话
        ContentValues valuesNumber = new ContentValues();
//      1、指定_id
        valuesNumber.put(ContactsContract.Data.RAW_CONTACT_ID, _id);

//      2、添加电话号码
        valuesNumber.put(ContactsContract.CommonDataKinds.Phone.NUMBER, "1234567890");

//      3、指定数据类型
        valuesNumber.put(ContactsContract.Data.MIMETYPE, 
                ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);

        resolver.insert(ContactsContract.Data.CONTENT_URI, valuesNumber);


//      添加Email
        ContentValues valuesEmail = new ContentValues();

//      1、指定_id
        valuesEmail.put(ContactsContract.Data.RAW_CONTACT_ID, _id);


//      2、添加数据
//      data1
        valuesEmail.put(ContactsContract.CommonDataKinds.Email.DATA1, "234567@163.com");

//      3、指定数据类型
        valuesEmail.put(ContactsContract.Data.MIMETYPE,
                ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE);
        resolver.insert(ContactsContract.Data.CONTENT_URI, valuesEmail);


    }

三:联系人数据库删除

//  删除联系人
    public void btnDelete(View view){
        int delete = resolver.delete(ContactsContract.Data.CONTENT_URI,
                ContactsContract.Data.RAW_CONTACT_ID+" = ?", new String[]{5+""});

        if(delete>=0){
            Toast.makeText(MainActivity.this, "删除成功", Toast.LENGTH_LONG).show();
        }else{
            Toast.makeText(MainActivity.this, "删除失败", Toast.LENGTH_LONG).show();
        }
    }

四:联系人数据库更新

//  更新联系人
    public void btnUpdate(View view){
        ContentValues valuesUpdate = new ContentValues();

        valuesUpdate.put(ContactsContract.CommonDataKinds.Phone.NUMBER, "4444444445678");


        int update = resolver.update(ContactsContract.Data.CONTENT_URI, valuesUpdate, 
                ContactsContract.Data.RAW_CONTACT_ID+" = ? and "+
                        ContactsContract.Data.MIMETYPE+" = ?", 
                        new String[]{6+"",5+""});

        if(update>=0){//更新成功
            Toast.makeText(MainActivity.this, "更新数据成功", Toast.LENGTH_LONG).show();
        }else{
            Toast.makeText(MainActivity.this, "更新数据失败", Toast.LENGTH_LONG).show();
        }

    }
作者:Soft_Po 发表于2016/8/14 23:51:32 原文链接
阅读:187 评论:0 查看评论

Android官方开发文档Training系列课程中文版:手势处理之ViewGroup的事件管理

$
0
0

原文地址:https://developer.android.com/training/gestures/viewgroup.html

在ViewGroup中处理触摸事件要格外小心,因为在ViewGroup中有很多子View,而这些子View对于不同的触摸事件来说是不同的目标。要确保每个View都正确的接收了相应的触摸事件。

在ViewGroup中拦截触摸事件

onInterceptTouchEvent()方法会在触摸事件到达ViewGroup的表面时调用,这包括内部的子View。如果onInterceptTouchEvent()返回了true,那么MotionEvent对象就会被拦截,这意味着该次事件不会传给子View,而是会传给ViewGroup本身的onTouchEvent()方法。

onInterceptTouchEvent()给了ViewGroup本身一个机会:在子View获得任何事件之前一个拦截该事件的机会。如果onInterceptTouchEvent()返回了true,那么原先处理该次事件的子View就会收到一个ACTION_CANCEL的事件,并且原先事件的剩余事件都会被传到该ViewGroup的onTouchEvent()方法中做常规处理。onInterceptTouchEvent()还可以返回false,这样的话,该次事件则会通过View树继续向下传递,直到到达目标View为止,目标View会在自己的onTouchEvent()方法中处理该次事件。

在下面的示例代码中,类MyViewGroup继承了ViewGroup,并包含了多个View,这些View我们在这里称之为子View,而MyViewGroup称为父容器View。如果你在水平方向上滑动手指,那么子View皆不会收到触摸事件。MyViewGroup会通过滚动它的内部来实现触摸事件的处理。不管如何,如果你按下了子View中的按钮,或者在垂直方向上滑动,那么ViewGroup则不会去拦截这些事件,因为子View是该次事件的目标View。在这些情况下,onInterceptTouchEvent()应该返回false,且MyViewGroup的onTouchEvent()方法也不会被调用。

public class MyViewGroup extends ViewGroup {

    private int mTouchSlop;

    ...

    ViewConfiguration vc = ViewConfiguration.get(view.getContext());
    mTouchSlop = vc.getScaledTouchSlop();

    ...

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        /*
         * This method JUST determines whether we want to intercept the motion.
         * If we return true, onTouchEvent will be called and we do the actual
         * scrolling there.
         */


        final int action = MotionEventCompat.getActionMasked(ev);

        // Always handle the case of the touch gesture being complete.
        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            // Release the scroll.
            mIsScrolling = false;
            return false; // Do not intercept touch event, let the child handle it
        }

        switch (action) {
            case MotionEvent.ACTION_MOVE: {
                if (mIsScrolling) {
                    // We're currently scrolling, so yes, intercept the
                    // touch event!
                    return true;
                }

                // If the user has dragged her finger horizontally more than
                // the touch slop, start the scroll

                // left as an exercise for the reader
                final int xDiff = calculateDistanceX(ev);

                // Touch slop should be calculated using ViewConfiguration
                // constants.
                if (xDiff > mTouchSlop) {
                    // Start scrolling!
                    mIsScrolling = true;
                    return true;
                }
                break;
            }
            ...
        }

        // In general, we don't want to intercept touch events. They should be
        // handled by the child view.
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        // Here we actually handle the touch event (e.g. if the action is ACTION_MOVE,
        // scroll this container).
        // This method will only be called if the touch event was intercepted in
        // onInterceptTouchEvent
        ...
    }
}

这里要注意,ViewGroup还提供了requestDisallowInterceptTouchEvent()方法。当子View不希望它的父容器及祖先容器拦截触摸事件时,ViewGroup会在 onInterceptTouchEvent()方法中对其进行调用,从而判断是否要拦截本次事件。

使用ViewConfiguration常量

在上面的代码中使用了ViewConfiguration来初始化一个名为mTouchSlop的变量。你可以使用ViewConfiguration来访问Android系统所使用的常用距离、速度及时间。

“mTouchSlop”引用了触摸事件在被拦截之前手指移动的以像素为单位的距离。Touch slop经常被用来在用户在执行触摸操作时防止产生意外滚动。

ViewConfiguration的另外两个常用方法是getScaledMinimumFlingVelocity()getScaledMaximumFlingVelocity()。这两个方法分别返回了用于初始化滚动的最小、最大的速度值。以每秒几像素为单位:

ViewConfiguration vc = ViewConfiguration.get(view.getContext());
private int mSlop = vc.getScaledTouchSlop();
private int mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
private int mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();

...

case MotionEvent.ACTION_MOVE: {
    ...
    float deltaX = motionEvent.getRawX() - mDownX;
    if (Math.abs(deltaX) > mSlop) {
        // A swipe occurred, do something
    }

...

case MotionEvent.ACTION_UP: {
    ...
    } if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity
            && velocityY < velocityX) {
        // The criteria have been satisfied, do something
    }
}

扩展子View的触控区域

Android提供的TouchDelegate使扩展子View的触控区域成为了可能。这对于子View本身特别小,而它的触控区域需要很大时很有用。如果需要的话,你也可以使用这种方式来缩小子View的触控区域。

在下面的示例中,ImageButton作为我们的”delegate view”(这里的意思是需要父容器扩展触控区域的那个View)。下面是示例的布局文件:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/parent_layout"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     tools:context=".MainActivity" >

     <ImageButton android:id="@+id/button"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:background="@null"
          android:src="@drawable/icon" />
</RelativeLayout>

下面的代码做了以下这些事情:

  • 获得父容器View,并Post一个Runnale对象到UI线程。这可以确保在调用getHitRect()方法之前父容器已经对子View完成了排布。getHitRect()会返回父容器坐标内当前View的点击矩阵(触控区域)。
  • 找到ImageButton,然后调用它的getHitRect()方法获得该View的触控边界。
  • 扩大ImageButton的触控区域。
  • 实例化一个TouchDelegate,将要扩展的触控区域矩阵与要扩展触控区域的ImageView作为参数传入。
  • TouchDelegate设置给父容器View,只有这样做,我们所触碰到的扩展区域才会被路由到子View上。

在TouchDelegate代理的范围内,父容器View将会接收所有的触摸事件。如果触摸事件发生在子View本身的触控区域内,那么父容器View会将所有的触摸事件传给子View处理:

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // Get the parent view
        View parentView = findViewById(R.id.parent_layout);

        parentView.post(new Runnable() {
            // Post in the parent's message queue to make sure the parent
            // lays out its children before you call getHitRect()
            @Override
            public void run() {
                // The bounds for the delegate view (an ImageButton
                // in this example)
                Rect delegateArea = new Rect();
                ImageButton myButton = (ImageButton) findViewById(R.id.button);
                myButton.setEnabled(true);
                myButton.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        Toast.makeText(MainActivity.this,
                                "Touch occurred within ImageButton touch region.",
                                Toast.LENGTH_SHORT).show();
                    }
                });

                // The hit rectangle for the ImageButton
                myButton.getHitRect(delegateArea);

                // Extend the touch area of the ImageButton beyond its bounds
                // on the right and bottom.
                delegateArea.right += 100;
                delegateArea.bottom += 100;

                // Instantiate a TouchDelegate.
                // "delegateArea" is the bounds in local coordinates of
                // the containing view to be mapped to the delegate view.
                // "myButton" is the child view that should receive motion
                // events.
                TouchDelegate touchDelegate = new TouchDelegate(delegateArea,
                        myButton);

                // Sets the TouchDelegate on the parent view, such that touches
                // within the touch delegate bounds are routed to the child.
                if (View.class.isInstance(myButton.getParent())) {
                    ((View) myButton.getParent()).setTouchDelegate(touchDelegate);
                }
            }
        });
    }
}
作者:u011064099 发表于2016/8/15 0:02:11 原文链接
阅读:235 评论:4 查看评论

Chromium为视频标签创建播放器的过程分析

$
0
0

       Chromium是通过WebKit解析网页内容的。当WebKit遇到<video>标签时,就会创建一个播放器实例。WebKit是平台无关的,而播放器实现是平台相关的。因此,WebKit并没有自己实现播放器,而仅仅是创建一个播放器接口。通过这个播放器接口,可以使用平台提供的播放器来播放视频的内容。这就简化了Chromium对视频标签的支持。本文接下来就分析Chromium为视频标签创建播放器的过程。

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

       以Android平台为例,它的SDK提供了一个MediaPlayer接口,用来播放视频。Chromium的目标就是为网页中的每一个<video>标签创建一个MediaPlayer实例,如图1所示:


图1 Chromium为<video>标签创建MediaPlayer的过程

       首先,WebKit会为网页中的每一个<video>标签创建一个类型为HTMLMediaElement的DOM节点。HTMLMediaElement类内部有一个WebMediaPlayerClientImpl接口。这个WebMediaPlayerClientImpl接口指向的是是一个运行在Render进程的Content模块中的一个WebMediaPlayerAndroid对象。这些WebMediaPlayerAndroid对象归一个称为RendererMediaPayerManager的对象管理。

       Render进程中的每一个WebMediaPlayerAndroid对象,在Browser进程中都有一个对应的WebMediaPlayerBridge对象。这些WebMediaPlayerBridge对象归一个称为BrowserMediaPayerManager的对象管理。每一个WebMediaPlayerBridge对象在Java层中又都对应有一个MediaPlayer对象。这些MediaPlayer对象描述的就是Android平台提供的播放器。

       接下来,我们就从WebKit解析<video>标签的属性开始,分析Chromium为它们创建MediaPlayer的过程,如下所示:

void HTMLMediaElement::parseAttribute(const QualifiedName& name, const AtomicString& value)
{
    if (name == srcAttr) {
        // Trigger a reload, as long as the 'src' attribute is present.
        if (!value.isNull()) {
            ......
            scheduleDelayedAction(LoadMediaResource);
        }
    } 

    ......
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       WebKit为网页的每一个标签创建了相应的DOM节点之后,就会调用这个DOM节点的成员函数parseAtrribute对它的属性进行解析。从前面Chromium网页DOM Tree创建过程分析一文可以容易知道,WebKit为<video>标签创建的DOM节点的实际类型为HTMLVideoElement。HTMLVideoElement类是从HTMLMediaElement类继承下来的,WebKit是调用后者的成同员函数parseAttribute来解析<video>的属性。

       我们假设<video>标签通过src属性设置了要播放的视频文件的URL。这时候HTMLMediaElement类的成员函数parseAttribute就会调用另外一个成员函数scheduleDelayedAction为<video>标签创建播放器,如下所示:

void HTMLMediaElement::scheduleDelayedAction(DelayedActionType actionType)
{
    .....

    if ((actionType & LoadMediaResource) && !(m_pendingActionFlags & LoadMediaResource)) {
        prepareForLoad();
        m_pendingActionFlags |= LoadMediaResource;
    }

    ......

    if (!m_loadTimer.isActive())
        m_loadTimer.startOneShot(0, FROM_HERE);
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       从前面的调用过程可以知道,参数actionType的值等于LoadMediaResource。这时候如果HTMLMediaElement类的成员变量m_pendingActionFlags的LoadMediaResource位等于0,那么就说明WebKit还没有为当前正在解析的<video>标签创建过播放器接口。于是接下来就会做两件事情:

       1. 调用另外一个成员函数prepareForLoad开始为当前正在解析的<video>标签创建图1所示的WebMediaPlayerClientImpl接口;

       2. 将成员变量m_pendingActionFlags的LoadMediaResource位设置为1,表示WebKit正在为当前正在解析的<video>标签创建过播放器接口,避免接下来出现重复创建的情况。

       HTMLMediaElement类的另外一个成员变量m_loadTimer描述的是一个定时器。如果这个定时器还没有被启动,那么HTMLMediaElement类的成员函数scheduleDelayedAction就会调用它的成员函数startOneShot马上进行启动。指定的启动时间单位为0,这意味着这个定时器会马上超时。超时后它将会调用HTMLMediaElement类的成员函数loadTimerFired。HTMLMediaElement类的成员函数loadTimerFired将会继续创建图1所示的WebMediaPlayerAndroid、WebMediaPlayerBridge和MediaPlayer接口。

       接下来我们就先分析HTMLMediaElement类的成员函数prepareForLoad的实现,如下所示:

void HTMLMediaElement::prepareForLoad()
{
    ......

    createMediaPlayer();

    ......
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       HTMLMediaElement类的成员函数prepareForLoad主要是调用另外一个成员函数createMediaPlayer为当前正在解析的<video>标签创建一个WebMediaPlayerClientImpl接口,如下所示:

void HTMLMediaElement::createMediaPlayer()
{
    ......

    m_player = MediaPlayer::create(this);

    ......
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       HTMLMediaElement类的成员函数createMediaPlayer调用MediaPlayer类的静态成员函数create创建了一个WebMediaPlayerClientImpl接口,并且保存在HTMLMediaElement类的成员变量m_player中。

       MediaPlayer类的静态成员函数create的实现如下所示:

static CreateMediaEnginePlayer createMediaEngineFunction = 0;

void MediaPlayer::setMediaEngineCreateFunction(CreateMediaEnginePlayer createFunction)
{
    ASSERT(createFunction);
    ASSERT(!createMediaEngineFunction);
    createMediaEngineFunction = createFunction;
}

PassOwnPtr<MediaPlayer> MediaPlayer::create(MediaPlayerClient* client)
{
    ASSERT(createMediaEngineFunction);
    return createMediaEngineFunction(client);
}
       这两个函数定义在文件external/chromium_org/third_party/WebKit/Source/platform/graphics/media/MediaPlayer.cpp中。

       MediaPlayer类的静态成员函数create调用全局变量createMediaEngineFunction描述的一个函数创建一个播放器接口返回给调用者。全局变量createMediaEngineFunction描述的函数实际上是WebMediaPlayerClientImpl类的静态成员函数create,它是通过调用MediaPlayer类的静态成员函数setMediaEngineCreateFunction设置的。

       WebMediaPlayerClientImpl类的静态成员函数create的实现如下所示:

PassOwnPtr<MediaPlayer> WebMediaPlayerClientImpl::create(MediaPlayerClient* client)
{
    return adoptPtr(new WebMediaPlayerClientImpl(client));
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/WebMediaPlayerClientImpl.cpp中。

       从这里可以看到,WebMediaPlayerClientImpl类的静态成员函数create返回的是一个WebMediaPlayerClientImpl对象。这个WebMediaPlayerClientImpl对象描述的就是WebKit层的播放器接口。

       这一步执行完成之后,WebKit就为当前正在解析的<video>标签创建了一个类型为WebMediaPlayerClientImpl的播放器接口。回到前面分析的HTMLMediaElement类的成员函数scheduleDelayedAction中,接下来它启动的定时器就会马上执行,也就HTMLMediaElement类的成员函数loadTimerFired会马上被调用。

       HTMLMediaElement类的成员函数loadTimerFired的实现如下所示:

void HTMLMediaElement::loadTimerFired(Timer<HTMLMediaElement>*)
{
    ......

    if (m_pendingActionFlags & LoadMediaResource) {
        if (m_loadState == LoadingFromSourceElement)
            loadNextSourceChild();
        else
            loadInternal();
    }

    m_pendingActionFlags = 0;
} 
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       前面分析的HTMLMediaElement类的成员函数scheduleDelayedAction已经将成员变量m_pendingActionFlags的LoadMediaResource位设置为1。这时候HTMLMediaElement类的成员函数loadTimerFired就会检查HTMLMediaElement类的另外一个成员变量m_loadState的值是否等于LoadingFromSourceElement。如果等于,那么就说明当前正在解析的<video>标签通过子元素<source>指定要播放的视频的URL。否则的话,就通过属性src指定要播放的视频的URL。

       前面我们假定了当前正在解析的<video>标签通过属性src指定要播放的视频的URL,因此HTMLMediaElement类的成员函数loadTimerFired接下来就会调用成员函数loadInternal继续为其创建其它的播放器接口,如下所示:

void HTMLMediaElement::loadInternal()
{
    ......

    selectMediaResource();
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       HTMLMediaElement类的成员函数loadInternal调用另外一个成员函数selectMediaResource为当前正在解析的<video>标签选择当前要播放的视频的URL。确定了当前要播放的视频的URL之后,就会加载视频元数据。有了这些元数据之后,就可以为其创建真正的播放器。

       HTMLMediaElement类的成员函数selectMediaResource的实现如下所示:

void HTMLMediaElement::selectMediaResource()
{
    ......

    enum Mode { attribute, children };

    .......
    Mode mode = attribute;
    if (!fastHasAttribute(srcAttr)) {
        // Otherwise, if the media element does not have a src attribute but has a source
        // element child, then let mode be children and let candidate be the first such
        // source element child in tree order.
        if (HTMLSourceElement* element = Traversal<HTMLSourceElement>::firstChild(*this)) {
            mode = children;
            ......
        } 
        .....
    }

    ......

    if (mode == attribute) {
        ......

        // If the src attribute's value is the empty string ... jump down to the failed step below
        KURL mediaURL = getNonEmptyURLAttribute(srcAttr);
        ......

        loadResource(mediaURL, contentType, String());
        .....
        return;
    }

    ......
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       HTMLMediaElement类的成员函数selectMediaResource所做的事情就要确定是从当前正在解析的<video>标签的src属性获得要加载的视频的URL,还是从它的子元素<source>获得要加载的视频的URL。

       如果当前正在解析的<video>标签设置了src属性,那么就会优先从这个属性获得要加载的视频的URL。有了这个URL之后,HTMLMediaElement类的成员函数selectMediaResource就会调用另外一个成员函数loadResource加载它所描述的视频的元数据。

       在我们这个情景中,当前正在解析的<video>标签设置了src属性。因此,接下来我们就继续分析HTMLMediaElement类的成员函数loadResource的实现,如下所示:

void HTMLMediaElement::loadResource(const KURL& url, ContentType& contentType, const String& keySystem)
{
    ......

    m_currentSrc = url;
    ......
    bool attemptLoad = true;

    if (url.protocolIs(mediaSourceBlobProtocol)) {
        if (isMediaStreamURL(url.string())) {
            m_userGestureRequiredForPlay = false;
        } else {
            m_mediaSource = HTMLMediaSource::lookup(url.string());

            if (m_mediaSource) {
                if (!m_mediaSource->attachToElement(this)) {
                    // Forget our reference to the MediaSource, so we leave it alone
                    // while processing remainder of load failure.
                    m_mediaSource = nullptr;
                    attemptLoad = false;
                }
            }
        }
    }

    if (attemptLoad && canLoadURL(url, contentType, keySystem)) {
        .......

        if (!m_havePreparedToPlay && !autoplay() && m_preload == MediaPlayer::None) {
            ......
            deferLoad();
        } else {
            startPlayerLoad();
        }
    } 

    ......
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       HTMLMediaElement类的成员函数loadResource首先将当前要播放的视频的URL保存在成员变量m_currentSrc中,接下来判断该URL的协议部分是否为“blob”。协议“blob”是“Binary Large OBject”的缩写,表示一组二进制数据。例如,我们手头上有一组表示一个Image的二进制数据,这时候可以调用URL.createObjectURL函数为这组二进制数据创建一个blob协议地址,然后再将该地址设置为一个<img>标签的src,这样就可以将图像显示出来。

       通过blob协议,还可以描述媒体数据。在WebKit中,媒体数据可以通过Media Source或者Media Stream API描述。Media Source API的设计初衷是让JavaScript能动态产生媒体流,然后交给<video>标签播放。Media Stream API是为WebRTC设计的,不仅可以使用<video>标签播放从本地摄像头采集的图像,还可以播放从网络发送过来的实时媒体流。关于Media Source 和Media Steam API的更详细信息,可以参考Media Source ExtensionsMedia Capture and Streams这两篇文档。

       如果当前要播放的视频的URL的协议部分为“blob”,HTMLMediaElement类的成员函数loadResource首先会检查它描述的是否是一个Media Stream。如果是的话,那么就会将HTMLMediaElement类的成员变量m_userGestureRequiredForPlay设置为false,表示后台Tab网页的视频可以自动播放。Render进程有一个“disable-gesture-requirement-for-media-playback”选项。当这个选项的值设置为false时,HTMLMediaElement类的成员变量m_userGestureRequiredForPlay就会被设置为true,表示后台Tab网页的视频不可以自动播放。不过,如果要播放的视频是一个Media Stream,那么不会受到此限制。关于Render进程的“disable-gesture-requirement-for-media-playback”启动选项的更多信息,可以参考Chrome 47 offers a flag to disable defer media playback in background tabs一文。

       如果当前要播放的视频的URL的协议部分为“blob”,但是它描述的不是一个Media Stream API,那么HTMLMediaElement类的成员函数loadResource再检查它是否是一个Media Source。如果是的话,就会将该Media Source作为当前正在解析的<video>标签的播放源。在这种情况下,WebKit不需要从网络上下载媒体数据回来,只需要从指定的Media Source读取回来就可以了。这时候本地变量attemptLoad的值会被设置为false。在其余情况下,本地变量attemptLoad的值保持为初始值true。

       在本地变量attemptLoad的值为true的情况下,HTMLMediaElement类的成员函数loadResource会调用另外一个成员函数canLoadURL继续检查当前要播放的视频的URL描述的内容是否为多媒体数据,以及该多媒体使用的编码方式是否被支持。如果检查通过,HTMLMediaElement类的成员函数loadResource再判断当前解析的<video>标签的是否需要preload和autoplay。如果不需要,那么就会调用成员函数deferLoad延迟加载要播放的视频的内容。否则的话,就会调用成员函数startPlayerLoad马上加载要播放的视频的内容回来。

       我们假设当前解析的<video>标签设置了autoplay,因此接下来我们就继续分析HTMLMediaElement类的成员函数startPlayerLoad的实现,如下所示:

void HTMLMediaElement::startPlayerLoad()
{
    .......

    KURL requestURL = m_currentSrc;
    ......

    m_player->load(loadType(), requestURL, corsMode());
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       从前面的分析可以知道,HTMLMediaElement类的成员变量m_player指向的是一个WebMediaPlayerClientImpl对象。HTMLMediaElement类的成员函数startPlayerLoad主要是调用这个WebMediaPlayerClientImpl对象的成员函数load加载当前要播放的视频的内容。

       WebMediaPlayerClientImpl类的成员函数load的实现如下所示:

void WebMediaPlayerClientImpl::load(WebMediaPlayer::LoadType loadType, const WTF::String& url, WebMediaPlayer::CORSMode corsMode)
{
    ......

    KURL kurl(ParsedURLString, url);
    m_webMediaPlayer = createWebMediaPlayer(this, kurl, frame);
    ...... 
   
    m_webMediaPlayer->load(loadType, kurl, corsMode);
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/WebMediaPlayerClientImpl.cpp中。

       WebMediaPlayerClientImpl类的成员函数load首先调用函数createWebMediaPlayer请求运行在当前进程(Render进程)中的Content模块创建一个播放器接口。这个Content模块的播放器接口会保存在WebMediaPlayerClientImpl类的成员变量m_webMediaPlayer中。有了Content模块的播放器接口之后,WebMediaPlayerClientImpl类的成员函数load再调用它的成员函数load请求加载当前要播放的视频的内容。

       接下来我们先分析Content模块的播放器接口的创建过程,也就是函数createWebMediaPlayer的实现,如下所示:

static PassOwnPtr<WebMediaPlayer> createWebMediaPlayer(WebMediaPlayerClient* client, const WebURL& url, LocalFrame* frame)
{
    WebLocalFrameImpl* webFrame = WebLocalFrameImpl::fromFrame(frame);
    ......
    return adoptPtr(webFrame->client()->createMediaPlayer(webFrame, url, client));
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/WebMediaPlayerClientImpl.cpp中。

       函数createWebMediaPlayer首先获得一个类型为blink::WebFrameClient的接口。这个接口是由WebKit的使用者设置给WebKit的,以便WebKit可以通过它执行一些平台相关的功能。在我们这个情景中,WebKit的使用者即为Render进程中的Content模块。Content模块中的RenderFrameImpl类实现了该接口,并且会设置给WebKit。因此,函数createWebMediaPlayer接下来就会调用它的成员函数createMediaPlayer创建一个播放器接口。

       RenderFrameImpl类的成员函数createMediaPlayer的实现如下所示:

blink::WebMediaPlayer* RenderFrameImpl::createMediaPlayer(
    blink::WebLocalFrame* frame,
    const blink::WebURL& url,
    blink::WebMediaPlayerClient* client) {
  blink::WebMediaStream web_stream(
      blink::WebMediaStreamRegistry::lookupMediaStreamDescriptor(url));
  if (!web_stream.isNull())
    return CreateWebMediaPlayerForMediaStream(url, client);

#if defined(OS_ANDROID)
  return CreateAndroidWebMediaPlayer(url, client);
#else
  ......
#endif  // defined(OS_ANDROID)
}
       这个函数定义在文件external/chromium_org/content/renderer/render_frame_impl.cc中。

       RenderFrameImpl类的成员函数createMediaPlayer首先判断当前要播放的视频的URL描述的是否是一个Media Stream。如果是的话,那么就会调用成员函数CreateWebMediaPlayerForMediaStream创建一个类型为WebMediaPlayerMS的播放器。这个类型为WebMediaPlayerMS的播放器是在WebRTC中实现的。我们不考虑这种情况。

       如果当前要播放的视频的URL描述的不是一个Media Stream,那么RenderFrameImpl类的成员函数createMediaPlayer就会调用另外一个成员函数CreateAndroidWebMediaPlayer创建另外一种类型的播放器,如下所示:

WebMediaPlayer* RenderFrameImpl::CreateAndroidWebMediaPlayer(
      const blink::WebURL& url,
      WebMediaPlayerClient* client) {
  GpuChannelHost* gpu_channel_host =
      RenderThreadImpl::current()->EstablishGpuChannelSync(
          CAUSE_FOR_GPU_LAUNCH_VIDEODECODEACCELERATOR_INITIALIZE);
  ......

  scoped_refptr<StreamTextureFactory> stream_texture_factory;
  if (SynchronousCompositorFactory* factory =
          SynchronousCompositorFactory::GetInstance()) {
    stream_texture_factory = factory->CreateStreamTextureFactory(routing_id_);
  } else {
    scoped_refptr<webkit::gpu::ContextProviderWebContext> context_provider =
        RenderThreadImpl::current()->SharedMainThreadContextProvider();
    ......

    stream_texture_factory = StreamTextureFactoryImpl::Create(
        context_provider, gpu_channel_host, routing_id_);
  }

  return new WebMediaPlayerAndroid(
      frame_,
      client,
      weak_factory_.GetWeakPtr(),
      GetMediaPlayerManager(),
      GetCdmManager(),
      stream_texture_factory,
      RenderThreadImpl::current()->GetMediaThreadMessageLoopProxy(),
      new RenderMediaLog());
}
       这个函数定义在文件external/chromium_org/content/renderer/render_frame_impl.cc中。

       RenderFrameImpl类的成员函数CreateAndroidWebMediaPlayer首先是调用RenderThreadImpl类的成员函数EstablishGpuChannelSync为接下来要创建的播放器创建一个到GPU进程的GPU通道。之所以要创建这个GPU通道,是因为Render要在GPU进程中创建一个纹理,然后将这个纹理封装为一个SurfaceTexture,作为Android平台的MediaPlayer的解码输出。也就是说,Render进程会通过这个SurfaceTexture接收Android平台的MediaPlayer的解码输出,然后再以纹理的形式渲染在网页上。GPU通道的创建过程,也就是RenderThreadImpl类的成员函数EstablishGpuChannelSync的实现,可以参考前面Chromium的GPU进程启动过程分析一文。

       RenderFrameImpl类的成员函数CreateAndroidWebMediaPlayer最终创建的是一个类型为WebMediaPlayerAndroid的播放器。创建这个类型为WebMediaPlayerAndroid的播放器需要用到两个重要的对象。一个是StreamTextureFactory对象,另一个是RendererMediaPlayerManager对象。前者用来创建前面所述的纹理。后者用来管理在Render进程中创建的播放器实例。

       在WebView的情况下,调用SynchronousCompositorFactory类的静态成员函数GetInstance会获得一个SynchronousCompositorFactory对象。在这种情况下,上述StreamTextureFactory对象通过调用这个SynchronousCompositorFactory对象的成员函数CreateStreamTextureFactory获得。我们不考虑这一种情况。

       在独立App的情况下,上述StreamTextureFactory对象实际上是一个StreamTextureFactoryImpl对象,它是通过调用StreamTextureFactoryImpl类的静态成员函数Create创建的,如下所示:

scoped_refptr<StreamTextureFactoryImpl> StreamTextureFactoryImpl::Create(
    const scoped_refptr<cc::ContextProvider>& context_provider,
    GpuChannelHost* channel,
    int frame_id) {
  return new StreamTextureFactoryImpl(context_provider, channel, frame_id);
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/stream_texture_factory_impl.cc中。

       从这里可以看到,StreamTextureFactoryImpl类的静态成员函数Create返回的是一个StreamTextureFactoryImpl对象。

       回到前面分析的RenderFrameImpl类的成员函数CreateAndroidWebMediaPlayer中,它创建了一个StreamTextureFactoryImpl对象之后,接下来又会调用另外一个成员函数GetMediaPlayerManager获得一个RendererMediaPlayerManager对象,如下所示:

RendererMediaPlayerManager* RenderFrameImpl::GetMediaPlayerManager() {
  if (!media_player_manager_) {
    media_player_manager_ = new RendererMediaPlayerManager(this);
    ......
  }
  return media_player_manager_;
}
       这个函数定义在文件external/chromium_org/content/renderer/render_frame_impl.cc中。

       从这里可以看到,RenderFrameImpl类的成员函数GetMediaPlayerManager返回的是成员变量media_player_manager_指向的是一个RendererMediaPlayerManager对象。如果这个RendererMediaPlayerManager对象还没有创建,那么就会先进行创建。

       再回到前面分析的RenderFrameImpl类的成员函数CreateAndroidWebMediaPlayer中,有了一个StreamTextureFactoryImpl对象和一个RendererMediaPlayerManager对象之后,它就会创建一个类型为WebMediaPlayerAndroid的播放器。

       类型为WebMediaPlayerAndroid的播放器的创建过程,也就是WebMediaPlayerAndroid类的构造函数的实现,如下所示:

WebMediaPlayerAndroid::WebMediaPlayerAndroid(
    blink::WebFrame* frame,
    blink::WebMediaPlayerClient* client,
    base::WeakPtr<WebMediaPlayerDelegate> delegate,
    RendererMediaPlayerManager* player_manager,
    RendererCdmManager* cdm_manager,
    scoped_refptr<StreamTextureFactory> factory,
    const scoped_refptr<base::MessageLoopProxy>& media_loop,
    media::MediaLog* media_log)
    : ......,
      player_manager_(player_manager),
      ......,
      stream_texture_factory_(factory),
      ...... {

  ......

  player_id_ = player_manager_->RegisterMediaPlayer(this);
  ......

  TryCreateStreamTextureProxyIfNeeded();
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

       WebMediaPlayerAndroid类的构造函数首先是将参数player_manager和factory描述的StreamTextureFactoryImpl对象和RendererMediaPlayerManager对象分别保存在成员变量player_manager_和stream_texture_factory_中。

       WebMediaPlayerAndroid类的构造函数接下来又会做两件事情:

       1. 调用上述RendererMediaPlayerManager对象的成员函数RegisterMediaPlayer将当前正在创建的WebMediaPlayerAndroid对象保存在其内部,并且为其分配一个ID。以后通过这个ID就可以在该RendererMediaPlayerManager对象中找到当前正在创建的WebMediaPlayerAndroid对象。

       2. 调用另外一个成员函数TryCreateStreamTextureProxyIfNeeded创建一个SurfaceTexture,以便后面可以用来接收Android平台的MediaPlayer的解码输出。

       关于WebMediaPlayerAndroid类的成员函数TryCreateStreamTextureProxyIfNeeded创建SurfaceTexture的过程,我们在接下来一篇文章中分析<video>标签的视频渲染过程时再分析。

       这一步执行完成之后,运行在Render进程中的Content模块就创建了一个类型为WebMediaPlayerAndroid的播放器接口。这个播放器接口会返回给WebKit层的WebMediaPlayerClientImpl类的成员函数load。WebMediaPlayerClientImpl类的成员函数load获得了这个播放器接口之后,就会调用它的成员函数load加载当前要播放的视频的内容,如下所示:

void WebMediaPlayerAndroid::load(LoadType load_type,
                                 const blink::WebURL& url,
                                 CORSMode cors_mode) {
  ......

  switch (load_type) {
    case LoadTypeURL:
      player_type_ = MEDIA_PLAYER_TYPE_URL;
      break;

    case LoadTypeMediaSource:
      player_type_ = MEDIA_PLAYER_TYPE_MEDIA_SOURCE;
      break;

    case LoadTypeMediaStream:
      CHECK(false) << "WebMediaPlayerAndroid doesn't support MediaStream on "
                      "this platform";
      return;
  }

  url_ = url;
  int demuxer_client_id = 0;
  if (player_type_ != MEDIA_PLAYER_TYPE_URL) {
    RendererDemuxerAndroid* demuxer =
        RenderThreadImpl::current()->renderer_demuxer();
    demuxer_client_id = demuxer->GetNextDemuxerClientID();

    media_source_delegate_.reset(new MediaSourceDelegate(
        demuxer, demuxer_client_id, media_loop_, media_log_));

    if (player_type_ == MEDIA_PLAYER_TYPE_MEDIA_SOURCE) {
      media::SetDecryptorReadyCB set_decryptor_ready_cb =
          media::BindToCurrentLoop(
              base::Bind(&WebMediaPlayerAndroid::SetDecryptorReadyCB,
                         weak_factory_.GetWeakPtr()));

      media_source_delegate_->InitializeMediaSource(
          base::Bind(&WebMediaPlayerAndroid::OnMediaSourceOpened,
                     weak_factory_.GetWeakPtr()),
          base::Bind(&WebMediaPlayerAndroid::OnNeedKey,
                     weak_factory_.GetWeakPtr()),
          set_decryptor_ready_cb,
          base::Bind(&WebMediaPlayerAndroid::UpdateNetworkState,
                     weak_factory_.GetWeakPtr()),
          base::Bind(&WebMediaPlayerAndroid::OnDurationChanged,
                     weak_factory_.GetWeakPtr()));
      InitializePlayer(url_, frame_->document().firstPartyForCookies(),
                       true, demuxer_client_id);
    }
  } else {
    info_loader_.reset(
        new MediaInfoLoader(
            url,
            cors_mode,
            base::Bind(&WebMediaPlayerAndroid::DidLoadMediaInfo,
                       weak_factory_.GetWeakPtr())));
    info_loader_->Start(frame_);
  }

  ......
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

       类型为WebMediaPlayerAndroid的播放器只能处理普通URL描述的媒体流,以及BLOB协议描述的类型为Media Source的媒体流,不能处理类型为Media Stream的媒体流(需要由WebRTC处理)。

       类型为Media Source的媒体流数据直接从指定的Media Source获得。WebMediaPlayerAndroid类的成员函数load将该Media Source封装在一个MediaSourceDelegate对象中,然后通过这个MediaSourceDelegate对象来获得要播放的视频的元数据。有了要播放的视频的元数据之后,就可以调用WebMediaPlayerAndroid类的成员函数InitializePlayer初始化当前正在处理的播放器。

       我们假设当前要播放的视频的URL是一个普通的URL,它描述的媒体流的元数据要通过一个MediaInfoLoader对象从网络上下载回来。下载完成后,WebMediaPlayerAndroid类的成员函数DidLoadMediaInfo会被调用,它的实现如下所示:

void WebMediaPlayerAndroid::DidLoadMediaInfo(
    MediaInfoLoader::Status status,
    const GURL& redirected_url,
    const GURL& first_party_for_cookies,
    bool allow_stored_credentials) {
  ......

  InitializePlayer(
      redirected_url, first_party_for_cookies, allow_stored_credentials, 0);

  ......
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

       WebMediaPlayerAndroid类的成员函数DidLoadMediaInfo会用下载回来的视频元数据初始化当前正在处理的播放器。这同样是通过调用WebMediaPlayerAndroid类的成员函数InitializePlayer进行的,如下所示:

void WebMediaPlayerAndroid::InitializePlayer(
    const GURL& url,
    const GURL& first_party_for_cookies,
    bool allow_stored_credentials,
    int demuxer_client_id) {
  ......
  player_manager_->Initialize(
      player_type_, player_id_, url, first_party_for_cookies, demuxer_client_id,
      frame_->document().url(), allow_stored_credentials);
  ......
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

       从前面的分析可以知道,WebMediaPlayerAndroid类的成员变量player_manager_指向的是一个RendererMediaPlayerManager对象。WebMediaPlayerAndroid类的成员函数InitializePlayer调用这个RendererMediaPlayerManager对象的成员函数Initialize对当前正在处理的播放器进行初始化,如下所示:

void RendererMediaPlayerManager::Initialize(
    MediaPlayerHostMsg_Initialize_Type type,
    int player_id,
    const GURL& url,
    const GURL& first_party_for_cookies,
    int demuxer_client_id,
    const GURL& frame_url,
    bool allow_credentials) {
  MediaPlayerHostMsg_Initialize_Params media_player_params;
  media_player_params.type = type;
  media_player_params.player_id = player_id;
  media_player_params.demuxer_client_id = demuxer_client_id;
  media_player_params.url = url;
  media_player_params.first_party_for_cookies = first_party_for_cookies;
  media_player_params.frame_url = frame_url;
  media_player_params.allow_credentials = allow_credentials;

  Send(new MediaPlayerHostMsg_Initialize(routing_id(), media_player_params));
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/renderer_media_player_manager.cc中。

       RendererMediaPlayerManager对象的成员函数Initialize向Browser进程发送一个类型为MediaPlayerHostMsg_Initialize的IPC消息,用来请求Browser进程为当前正在处理的类型为WebMediaPlayerAndroid的播放器创建一个由Android平台实现的播放器。

       Browser进程是通过MediaWebContentsObserver类的成员函数OnMediaPlayerMessageReceived接收类型为MediaPlayerHostMsg_Initialize的IPC消息的,如下所示:

bool MediaWebContentsObserver::OnMediaPlayerMessageReceived(
    const IPC::Message& msg,
    RenderFrameHost* render_frame_host) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(MediaWebContentsObserver, msg)
    ......
    IPC_MESSAGE_FORWARD(MediaPlayerHostMsg_Initialize,
                        GetMediaPlayerManager(render_frame_host),
    ......
    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()
  return handled;
}
       这个函数定义在文件external/chromium_org/content/browser/media/media_web_contents_observer.cc中。

       MediaWebContentsObserver类的成员函数OnMediaPlayerMessageReceived会将类型为MediaPlayerHostMsg_Initialize的IPC消息分发给运行在Browser进程中的一个BrowserMediaPlayerManager对象的成员函数OnInitialize处理。这个BrowserMediaPlayerManager对象负责管理在Browser进程中创建的播放器实例,它与运行在Render进程中的RendererMediaPlayerManager对象是对应的,不过后者用来管理在Render进程中创建的播放器实例。

       BrowserMediaPlayerManager类的成员函数OnInitialize的实现如下所示:

void BrowserMediaPlayerManager::OnInitialize(
    const MediaPlayerHostMsg_Initialize_Params& media_player_params) {
  ......

  MediaPlayerAndroid* player = CreateMediaPlayer(
      media_player_params,

      host->GetBrowserContext()->IsOffTheRecord(), this,
      host->browser_demuxer_android());

  ......

  AddPlayer(player);
}

       这个函数定义在文件external/chromium_org/content/browser/media/android/browser_media_player_manager.cc中。

       BrowserMediaPlayerManager类的成员函数OnInitialize主要是调用成员函数CreateMediaPlayer创建一个类型为MediaPlayerBridge的播放器实例,并且调用另外一个成员函数AddPlayer将这个播放器实例保存在内部。

       BrowserMediaPlayerManager类的成员函数CreateMediaPlayer的实现如下所示:

MediaPlayerAndroid* BrowserMediaPlayerManager::CreateMediaPlayer(
    const MediaPlayerHostMsg_Initialize_Params& media_player_params,
    bool hide_url_log,
    MediaPlayerManager* manager,
    BrowserDemuxerAndroid* demuxer) {
  switch (media_player_params.type) {
    case MEDIA_PLAYER_TYPE_URL: {
      const std::string user_agent = GetContentClient()->GetUserAgent();
      MediaPlayerBridge* media_player_bridge = new MediaPlayerBridge(
          media_player_params.player_id,
          media_player_params.url,
          media_player_params.first_party_for_cookies,
          user_agent,
          hide_url_log,
          manager,
          base::Bind(&BrowserMediaPlayerManager::OnMediaResourcesRequested,
                     weak_ptr_factory_.GetWeakPtr()),
          base::Bind(&BrowserMediaPlayerManager::OnMediaResourcesReleased,
                     weak_ptr_factory_.GetWeakPtr()),
          media_player_params.frame_url,
          media_player_params.allow_credentials);
      BrowserMediaPlayerManager* browser_media_player_manager =
          static_cast<BrowserMediaPlayerManager*>(manager);
      ContentViewCoreImpl* content_view_core_impl =
          static_cast<ContentViewCoreImpl*>(ContentViewCore::FromWebContents(
              browser_media_player_manager->web_contents_));
      if (!content_view_core_impl) {
        // May reach here due to prerendering. Don't extract the metadata
        // since it is expensive.
        // TODO(qinmin): extract the metadata once the user decided to load
        // the page.
        browser_media_player_manager->OnMediaMetadataChanged(
            media_player_params.player_id, base::TimeDelta(), 0, 0, false);
      } else if (!content_view_core_impl->ShouldBlockMediaRequest(
            media_player_params.url)) {
        media_player_bridge->Initialize();
      }
      return media_player_bridge;
    }

    case MEDIA_PLAYER_TYPE_MEDIA_SOURCE: {
      return new MediaSourcePlayer(
          media_player_params.player_id,
          manager,
          base::Bind(&BrowserMediaPlayerManager::OnMediaResourcesRequested,
                     weak_ptr_factory_.GetWeakPtr()),
          base::Bind(&BrowserMediaPlayerManager::OnMediaResourcesReleased,
                     weak_ptr_factory_.GetWeakPtr()),
          demuxer->CreateDemuxer(media_player_params.demuxer_client_id),
          media_player_params.frame_url);
    }
  }

  NOTREACHED();
  return NULL;
}

       这个函数定义在文件external/chromium_org/content/browser/media/android/browser_media_player_manager.cc中。

       BrowserMediaPlayerManager类的成员函数CreateMediaPlayer同样是只会为普通URL描述的视频以及类型为Media Source的视频创建播放器。这里我们只考虑普通URL描述的视频的情况。这时候BrowserMediaPlayerManager类的成员函数CreateMediaPlayer会创建一个类型为MediaPlayerBridge的播放器,并且在视频所加载在的网页可见的情况下,调用它的成员函数Initialize对它进行初始化,也就是获取视频元数据。如果视频所加载在的网页当前不可见,那么获取视频元数据的操作可以延后执行,也就是等到下次可见时再执行。

       我们假设视频所加载在的网页当前是可见的。接下来我们就继续分析类型为MediaPlayerBridge的播放器的初始化过程,也就是MediaPlayerBridge类的成员函数Initialize的实现,如下所示:

void MediaPlayerBridge::Initialize() {
  ......

  media::MediaResourceGetter* resource_getter =
      manager()->GetMediaResourceGetter();
  ......

  // Start extracting the metadata immediately if the request is anonymous.
  // Otherwise, wait for user credentials to be retrieved first.
  if (!allow_credentials_) {
    ExtractMediaMetadata(url_.spec());
    return;
  }

  resource_getter->GetCookies(url_,
                              first_party_for_cookies_,
                              base::Bind(&MediaPlayerBridge::OnCookiesRetrieved,
                                         weak_factory_.GetWeakPtr()));
}
       这个函数定义在文件external/chromium_org/media/base/android/media_player_bridge.cc中。

       MediaPlayerBridge类的成员变量allow_credentials_是一个布尔变量。当它的值等于false的时候,表示视频元数据可以通过匿名方式获取。在这种情况下,MediaPlayerBridge类的成员函数Initialize就会直接调用另外一个成员函数ExtractMediaMetadata获取视频元数据。

       当MediaPlayerBridge类的成员变量allow_credentials_的值等于true的时候,表示视频元数据要有凭证才可以获取。在这种情况下,MediaPlayerBridge类的成员函数Initialize先通过调用一个MediaResourceGetter对象的成员函数GetCookies获取该凭证。获得了这个凭证之后,MediaPlayerBridge类的成员函数OnCookiesRetrieved会被回调。

       MediaPlayerBridge类的成员函数OnCookiesRetrieved被回调用的时候,它同样是会调用成员函数ExtractMediaMetadata去获取视频元数据。为简单起见,我们假设视频元数据可以通过匿名方式获取。因此,接下来我们就继续分析MediaPlayerBridge类的成员函数ExtractMediaMetadata的实现,以及了解视频元数据获取的过程,如下所示:

void MediaPlayerBridge::ExtractMediaMetadata(const std::string& url) {
  int fd;
  int64 offset;
  int64 size;
  if (InterceptMediaUrl(url, &fd, &offset, &size)) {
    manager()->GetMediaResourceGetter()->ExtractMediaMetadata(
        fd, offset, size,
        base::Bind(&MediaPlayerBridge::OnMediaMetadataExtracted,
                   weak_factory_.GetWeakPtr()));
  } else {
    manager()->GetMediaResourceGetter()->ExtractMediaMetadata(
        url, cookies_, user_agent_,
        base::Bind(&MediaPlayerBridge::OnMediaMetadataExtracted,
                   weak_factory_.GetWeakPtr()));
  }
}
       这个函数定义在文件external/chromium_org/media/base/android/media_player_bridge.cc中。

       MediaPlayerBridge类的成员函数ExtractMediaMetadata首先是调用成员函数InterceptMediaUrl检查当前要播放的视频是否已经存在本地,也就是之前下载过。如果是的话,就会直接从该文件读取视频元数据回来。否则的话,就会通过网络请求视频元数据。无论是哪一种方式,一旦得到视频元数据之后,就会回调用MediaPlayerBridge类的成员函数OnMediaMetadataExtracted进行后续处理。

       MediaPlayerBridge类的成员函数OnMediaMetadataExtracted的实现如下所示:

void MediaPlayerBridge::OnMediaMetadataExtracted(
    base::TimeDelta duration, int width, int height, bool success) {
  if (success) {
    duration_ = duration;
    width_ = width;
    height_ = height;
  }
  manager()->OnMediaMetadataChanged(
      player_id(), duration_, width_, height_, success);
}
       这个函数定义在文件external/chromium_org/media/base/android/media_player_bridge.cc中。

       获取的视频元数据包括视频持续时间、宽度以及高度。这些数据会分别保存在MediaPlayerBridge类的成员变量duration_、width_和height_中。

       MediaPlayerBridge类的成员函数OnMediaMetadataExtracted最后还会通知Browser进程中的BrowserMediaPlayerManager对象,当前正在处理的播放器已经获得了视频元数据。这个BrowserMediaPlayerManager对象是通过调用MediaPlayerBridge类的成员函数manager获得的,并且是通过调用它的成员函数OnMediaMetadataChanged对它进行通知的。

       BrowserMediaPlayerManager类的成员函数OnMediaMetadataChanged的实现如下所示:

void BrowserMediaPlayerManager::OnMediaMetadataChanged(
    int player_id, base::TimeDelta duration, int width, int height,
    bool success) {
  Send(new MediaPlayerMsg_MediaMetadataChanged(
      RoutingID(), player_id, duration, width, height, success));
  ......
}
       这个函数定义在文件external/chromium_org/content/browser/media/android/browser_media_player_manager.cc中。

       BrowserMediaPlayerManager类的成员函数OnMediaMetadataChanged主要是向Render进程发送一个类型为MediaPlayerMsg_MediaMetadataChanged的IPC消息,通知为ID为player_id的播放器已经获得了要播放的视频的元数据。

       Render进程是通过RendererMediaPlayerManager类的成员函数OnMessageReceived接收类型为MediaPlayerMsg_MediaMetadataChanged的IPC消息的,如下所示:

bool RendererMediaPlayerManager::OnMessageReceived(const IPC::Message& msg) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(RendererMediaPlayerManager, msg)
    IPC_MESSAGE_HANDLER(MediaPlayerMsg_MediaMetadataChanged,
                        OnMediaMetadataChanged)
    ......
  IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()
  return handled;
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/renderer_media_player_manager.cc中。

       RendererMediaPlayerManager类的成员函数OnMessageReceived将类型为MediaPlayerMsg_MediaMetadataChanged的IPC消息分发给另外一个成员函数OnMediaMetadataChanged处理,如下所示:

void RendererMediaPlayerManager::OnMediaMetadataChanged(
    int player_id,
    base::TimeDelta duration,
    int width,
    int height,
    bool success) {
  WebMediaPlayerAndroid* player = GetMediaPlayer(player_id);
  if (player)
    player->OnMediaMetadataChanged(duration, width, height, success);
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/renderer_media_player_manager.cc中。

       RendererMediaPlayerManager类的成员函数OnMediaMetadataChanged首先根据参数player_id获得之前创建的一个类型为WebMediaPlayerAndroid的播放器,然后调用这个播放器的成员函数OnMediaMetadataChanged通知它,当前要播放的视频元数据已经获取回来了。

       WebMediaPlayerAndroid类的成员函数OnMediaMetadataChanged的实现如下所示:

void WebMediaPlayerAndroid::OnMediaMetadataChanged(
    const base::TimeDelta& duration, int width, int height, bool success) {
  ......

  if (ready_state_ != WebMediaPlayer::ReadyStateHaveEnoughData) {
    UpdateReadyState(WebMediaPlayer::ReadyStateHaveMetadata);
    UpdateReadyState(WebMediaPlayer::ReadyStateHaveEnoughData);
  }

  ......
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

       WebMediaPlayerAndroid类的成员变量read_state_描述的播放器的状态。如果它的状态不等于WebMediaPlayer::ReadyStateHaveEnoughData,即还没有足够的数据开始播放视频,那么WebMediaPlayerAndroid类的成员函数OnMediaMetadataChanged此时就会先将状态更改为WebMediaPlayer::ReadyStateHaveMetadata,然后再更改为WebMediaPlayer::ReadyStateHaveEnoughData。这都是通过调用WebMediaPlayerAndroid类的成员函数UpdateReadyState实现的。

       WebMediaPlayerAndroid类的成员函数UpdateReadyState的实现如下所示:

void WebMediaPlayerAndroid::UpdateReadyState(
    WebMediaPlayer::ReadyState state) {
  ready_state_ = state;
  client_->readyStateChanged();
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

       WebMediaPlayerAndroid类的成员函数UpdateReadyState除了将Content模块中的播放器的状态设置为参数state描述的值之外,还会通知WebKit层中的播放器,也就是一个WebMediaPlayerClientImpl对象,它的状态发生了变化。

       上述WebMediaPlayerClientImpl对象保存在WebMediaPlayerAndroid类的成员变量client_中,通过调用它的成员函数readyStateChanged可以通知它,播放器状态发生了变化,如下所示:

void WebMediaPlayerClientImpl::readyStateChanged()
{
    m_client->mediaPlayerReadyStateChanged();
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/WebMediaPlayerClientImpl.cpp中。

       从前面的分析可以知道,WebMediaPlayerClientImpl类的成员变量m_client指向的是一个HTMLMediaElement对象。这个HTMLMediaElement对象描述的就是要播放视频的<video>标签。WebMediaPlayerClientImpl类的成员函数readyStateChanged调用这个HTMLMediaElement对象的成员函数mediaPlayerReadyStateChanged通知它,播放器状态发生了变化,如下所示:

void HTMLMediaElement::mediaPlayerReadyStateChanged()
{
    setReadyState(static_cast<ReadyState>(webMediaPlayer()->readyState()));
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       HTMLMediaElement类的成员函数mediaPlayerReadyStateChanged首先获得Content模块中的播放器的当前状态,然后再调用另外一个成员函数setReadyState将这个状态保存在内部。从前面的分析可以知道,Content模块中的播放器的当前状态为WebMediaPlayer::ReadyStateHaveEnoughData,对应于在WebKit中定义的状态HAVE_ENOUGH_DATA。

       HTMLMediaElement类的成员函数setReadyState的实现如下所示:

void HTMLMediaElement::setReadyState(ReadyState state)
{
    ......

    bool tracksAreReady = textTracksAreReady();
    ......

    if (tracksAreReady)
        m_readyState = newState;
    else {
        // If a media file has text tracks the readyState may not progress beyond HAVE_FUTURE_DATA until
        // the text tracks are ready, regardless of the state of the media file.
        if (newState <= HAVE_METADATA)
            m_readyState = newState;
        else
            m_readyState = HAVE_CURRENT_DATA;
    }

    ......

    updatePlayState();
    
    ......
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       HTMLMediaElement类的成员变量m_readyState用来描述WebKit层中的播放器的状态。HTMLMediaElement类的成员函数setReadyState首先调用成员函数textTracksAreReady检查当前要播放的视频是否存在类型为Text的Track,也就是字幕。如果存在,并且这些Track都已经Ready,那么它的返回值就会等于true。另一方面,如果不存在类型为Text的Track,那么调用HTMLMediaElement类的成员函数textTracksAreReady得到的返回值也会等于true。

       在HTMLMediaElement类的成员函数textTracksAreReady的返回值等于true的情况下,HTMLMediaElement类的成员函数setReadyState才会将WebKit层中的播放器的状态设置为参数state的值,也就是将它的状态保持与Content模块中的播放器一致。否则的话,至多会将WebKit层中的播放器的状态设置为HAVE_CURRENT_DATA。这个HAVE_CURRENT_DATA状态不能让播放器开始播放视频。

       我们假设当前要播放的视频不存在类型为Text的Track。因此,这时候WebKit层中的播放器的状态将会被设置为HAVE_ENOUGH_DATA,也就是HTMLMediaElement类的成员变量m_readyState会被设置为HAVE_ENOUGH_DATA。

       HTMLMediaElement类的成员函数setReadyState最后调用成员函数updatePlayState开始播放视频,如下所示:

void HTMLMediaElement::updatePlayState()
{
    ......

    bool shouldBePlaying = potentiallyPlaying();
    bool playerPaused = m_player->paused();

    .....

    if (shouldBePlaying) {
        ......

        if (playerPaused) {
            ......

            m_player->play();
        }

        ......
    }

    ......
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/core/html/HTMLMediaElement.cpp中。

       HTMLMediaElement类的成员函数updatePlayState首先调用另外一个成员函数potentiallyPlaying检查WebKit层中的播放器的状态。如果WebKit层中的播放器的状态表明它已经获得了足够的视频数据,并且视频还没有播放结束,以及没有被用户中止,也没有出现错误等,那么HTMLMediaElement类的成员函数potentiallyPlaying的返回值就会等于true。在这种情况下,如果Content模块中的播放器目前处于暂停状态,那么就可以通知它开始播放视频了。这是通过调用HTMLMediaElement类的成员变量m_player指向的一个WebMediaPlayerClientImpl对象的成员函数play实现的。

       从前面的分析可以知道,在我们这个情景中,通知Content模块中的播放器开始播放视频的条件是满足的,因此接下来我们就继续分析WebMediaPlayerClientImpl类的成员函数play的实现,如下所示:

void WebMediaPlayerClientImpl::play()
{
    if (m_webMediaPlayer)
        m_webMediaPlayer->play();
}
       这个函数定义在文件external/chromium_org/third_party/WebKit/Source/web/WebMediaPlayerClientImpl.cpp中。

       从前面的分析可以知道,WebMediaPlayerClientImpl类的成员变量m_webMediaPlayer指向的是一个WebMediaPlayerAndroid对象。这个WebMediaPlayerAndroid对象描述的就是Content模块中的播放器实例。WebMediaPlayerClientImpl类的成员函数play调用这个WebMediaPlayerAndroid对象的成员函数play通知它开始播放视频。

       WebMediaPlayerAndroid类的成员函数play的实现如下所示:

void WebMediaPlayerAndroid::play() {
  ......

  TryCreateStreamTextureProxyIfNeeded();
  // There is no need to establish the surface texture peer for fullscreen
  // video.
  if (hasVideo() && needs_establish_peer_ &&
      !player_manager_->IsInFullscreen(frame_)) {
    EstablishSurfaceTexturePeer();
  }

  if (paused())
    player_manager_->Start(player_id_);
  ......
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/webmediaplayer_android.cc中。

       WebMediaPlayerAndroid类的成员函数play首先调用成员函数TryCreateStreamTextureProxyIfNeeded检查当前正在处理的WebMediaPlayerAndroid对象是否已经创建过一个纹理对象。如果还没有创建,那么就会请求GPU进程进行创建。

       WebMediaPlayerAndroid类的成员函数play接下来又检查是否需要将上述纹理对象封装为一个SurfaceTexture,然后再将该SurfaceTexture设置为Android平台的MediaPlayer的解码输出。在满足以下两个条件时,就需要进行封装和设置,也就是调用另外一个成员函数EstablishSurfaceTexturePeer:

       1. 要播放的媒体包含有视频,即调用成员函数hasVideo得到的返回值等于true。

       2. 要播放的视频不是全屏模式,这时候成员变量needs_establish_peer_的值等于true,以及调用调用成员变量player_manager_指向的RendererMediaPlayerManager对象的成员函数IsInFullscreen得到的返回值等于false。

       在我们这个情景中,上述两个条件都是满足的。不过,WebMediaPlayerAndroid类的TryCreateStreamTextureProxyIfNeeded和EstablishSurfaceTexturePeer的实现我们在接下来的一篇文章中再详细分析。

       WebMediaPlayerAndroid类的成员函数play最后检查当前正在处理的WebMediaPlayerAndroid对象描述的播放器是否处于暂停状态。如果是的话,那么就会调用成员变量player_manager_指向的RendererMediaPlayerManager对象的成员函数Start启动该播放器。

       接下来我们就继续分析RendererMediaPlayerManager类的成员函数Start的实现,以便了解类型为WebMediaPlayerAndroid的播放器的启动过程,如下所示:

void RendererMediaPlayerManager::Start(int player_id) {
  Send(new MediaPlayerHostMsg_Start(routing_id(), player_id));
}
       这个函数定义在文件external/chromium_org/content/renderer/media/android/renderer_media_player_manager.cc中。

       RendererMediaPlayerManager类的成员函数Start向Browser进程发送一个类型为MediaPlayerHostMsg_Start的IPC消息,用来请求启动ID值为player_id的播放器。

       Browser进程通过MediaWebContentsObserver类的成员函数OnMediaPlayerMessageReceived接收类型为MediaPlayerHostMsg_Start的IPC消息,如下所示:

bool MediaWebContentsObserver::OnMediaPlayerMessageReceived(
    const IPC::Message& msg,
    RenderFrameHost* render_frame_host) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(MediaWebContentsObserver, msg)
    ......
    IPC_MESSAGE_FORWARD(MediaPlayerHostMsg_Start,
                        GetMediaPlayerManager(render_frame_host),
                        BrowserMediaPlayerManager::OnStart)
    ......
    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()
  return handled;
}
       这个函数定义在文件external/chromium_org/content/browser/media/media_web_contents_observer.cc中。

       MediaWebContentsObserver类的成员函数OnMediaPlayerMessageReceived会将类型为MediaPlayerHostMsg_Start的IPC消息分发给运行在Browser进程中的一个BrowserMediaPlayerManager对象的成员函数OnStart处理,如下所示:

void BrowserMediaPlayerManager::OnStart(int player_id) {
  MediaPlayerAndroid* player = GetPlayer(player_id);
  ......
  player->Start();
  ......
}
       这个函数定义在文件external/chromium_org/content/browser/media/android/browser_media_player_manager.cc中。

       BrowserMediaPlayerManager类的成员函数OnStart首先调用成员函数GetPlayer获得与参数player_id对应的一个MediaPlayerBridge对象,然后调用这个MediaPlayerBridge对象的成员函数Start启动它所描述的播放器。注意,这里获得的MediaPlayerBridge对象使用一个类型为MediaPlayerAndroid的指针引用,这是因为MediaPlayerBridge类是从类MediaPlayerAndroid继承下来的。

       接下来我们就继续分析MediaPlayerBridge类的成员函数Start的实现,如下所示:

void MediaPlayerBridge::Start() {
  if (j_media_player_bridge_.is_null()) {
    pending_play_ = true;
    Prepare();
  } else {
    if (prepared_)
      StartInternal();
    else
      pending_play_ = true;
  }
}
       这个函数定义在文件external/chromium_org/media/base/android/media_player_bridge.cc中。

       MediaPlayerBridge类的成员变量j_media_player_bridge_是一个类型为jobject的引用,它是用来指向在Java层创建的一个MediaPlayerBridge对象的。如果这个MediaPlayerBridge对象还没有创建,那么MediaPlayerBridge类的成员变量j_media_player_bridge_的值就会等于NULL。这种情况说明我们还没有为<video>标签创建一个真正的播放器。这个真正在的播放器是由Android系统提供的。这时候就会调用另外一个成员函数Prepare在Java层创建这个由Android系统提供的播放器,并且让这个播放器准备就绪播放。

       一旦在Java层就准好由Android系统提供的播放器,那么Chromium会通过JNI回到C/C++层,调用MediaPlayerBridge类的成员函数StartInternal记动该播放器,并且将MediaPlayerBridge类的成员变量prepared的值设置为true。

       另一方面,如果此时已经在Java层准备就绪好Android系统提供的播放器,那么MediaPlayerBridge类的成员函数Start就会直接调用另外一个成员函数StartInternal启动该播放器。

       还存在第三种情况,MediaPlayerBridge类的成员变量j_media_player_bridge_已经指向一个Java层的MediaPlayerBridge对象,但是还没有准备好Android系统提供的播放器。这时候MediaPlayerBridge类的成员函数Start就会将另外一个成员变量pending_play_的值设置为true,表示等到Android系统提供的播放器准备就绪后,要启动该播放器。

       我们假设此时MediaPlayerBridge类的成员变量j_media_player_bridge_还没有指向一个Java层的MediaPlayerBridge对象,接下来MediaPlayerBridge类的成员函数Prepare就会被调用,它的实现如下所示:

void MediaPlayerBridge::Prepare() {
  ......
  CreateJavaMediaPlayerBridge();
  ......

  SetDataSource(url_.spec());
}
       这个函数定义在文件external/chromium_org/media/base/android/media_player_bridge.cc中。

       MediaPlayerBridge类的成员函数Prepare首先调用成员函数CreateJavaMediaPlayerBridge创建一个Java层的MediaPlayerBridge对象,然后再调用另外一个成员函数SetDataSource在Java层创建一个由Android系统实现的播放器,并且将要播放的视频的URL设置给该播放器。

       MediaPlayerBridge类的成员函数CreateJavaMediaPlayerBridge的实现如下所示:

void MediaPlayerBridge::CreateJavaMediaPlayerBridge() {
  JNIEnv* env = base::android::AttachCurrentThread();
  ......

  j_media_player_bridge_.Reset(Java_MediaPlayerBridge_create(
      env, reinterpret_cast<intptr_t>(this)));

  ......
}
       这个函数定义在文件external/chromium_org/media/base/android/media_player_bridge.cc中。

       MediaPlayerBridge类的成员函数CreateJavaMediaPlayerBridge通过JNI调用Java层的MediaPlayerBridge类的静态成员函数create创建了一个Java层的MediaPlayerBridge对象,并且保存在成员变量j_media_player_bridge_中。

       Java层的MediaPlayerBridge类的静态成员函数create的实现如下所示:

public class MediaPlayerBridge {
    ......

    private long mNativeMediaPlayerBridge;

    @CalledByNative
    private static MediaPlayerBridge create(long nativeMediaPlayerBridge) {
        return new MediaPlayerBridge(nativeMediaPlayerBridge);
    }

    protected MediaPlayerBridge(long nativeMediaPlayerBridge) {
        mNativeMediaPlayerBridge = nativeMediaPlayerBridge;
    }

    ......
}
       这个函数定义在文件external/chromium_org/media/base/android/java/src/org/chromium/media/MediaPlayerBridge.java中。

       从这里就可以看到,Java层的MediaPlayerBridge类的静态成员函数create创建的是一个Java层的MediaPlayerBridge对象。在创建这个MediaPlayerBridge对象对象的时候,需要使用到参数nativeMediaPlayerBridge。

       从前面的调用过程可以知道,参数nativeMediaPlayerBridge描述的是C++层的一个MediaPlayerBridge对象的地址。这个地址会保存在Java层创建的MediaPlayerBridge对象的成员变量mNativeMediaPlayerBridge中。通过这种方式,就可以让Java层的MediaPlayerBridge与C++层的MediaPlayerBridge对象一一对应起来。

       这一步执行完成后,回到前面分析C++层的MediaPlayerBridge类的成员函数Prepare中,它接下来调用另外一个成员函数SetDataSource在Java层创建一个由Android系统提供的播放器,以及将要播放的视频的URL设置给该播放器,如下所示:

void MediaPlayerBridge::SetDataSource(const std::string& url) {
  ......

  JNIEnv* env = base::android::AttachCurrentThread();
  ......

  int fd;
  int64 offset;
  int64 size;
  if (InterceptMediaUrl(url, &fd, &offset, &size)) {
    if (!Java_MediaPlayerBridge_setDataSourceFromFd(
        env, j_media_player_bridge_.obj(), fd, offset, size)) {
      ......
      return;
    }
  } else {
    // Create a Java String for the URL.
    ScopedJavaLocalRef<jstring> j_url_string =
        ConvertUTF8ToJavaString(env, url);

    ......

    ScopedJavaLocalRef<jstring> j_cookies = ConvertUTF8ToJavaString(
        env, cookies_);
    ScopedJavaLocalRef<jstring> j_user_agent = ConvertUTF8ToJavaString(
        env, user_agent_);

    if (!Java_MediaPlayerBridge_setDataSource(
        env, j_media_player_bridge_.obj(), j_context, j_url_string.obj(),
        j_cookies.obj(), j_user_agent.obj(), hide_url_log_)) {
      ......
      return;
    }
  }

  ......

  if (!Java_MediaPlayerBridge_prepareAsync(env, j_media_player_bridge_.obj()))
    OnMediaError(MEDIA_ERROR_FORMAT);
}
       这个函数定义在文件external/chromium_org/media/base/android/media_player_bridge.cc中。

       C++层的MediaPlayerBridge类的成员函数SetDataSource首先调用成员函数InterceptMediaUrl检查参数url描述的URL对应的视频是否已经在本地以文件的形式存在。如果存在,那么就通过JNI调用Java层的MediaPlayerBridge类的成员函数setDataSourceFromFd将该视频文件设置为Android系统提供的播放器的视频源。

       另一方面,如果参数url描述的URL对应的视频在本地不存在,那么C++层的MediaPlayerBridge类的成员函数SetDataSource就会通过JNI调用Java层的MediaPlayerBridge类的成员函数setDataSource,以便将要播放的视频的URL设置给Android系统提供的播放器。一起设置给Android系统提供的播放器的数据还包括当前网页所属网站的Cookie,以及用户凭证等。这是因为Android系统提供的播放器从服务器下载视频时,可能需要用到这些信息。

       设置好Android系统提供的播放器的视频源之后,C++层的MediaPlayerBridge类的成员函数SetDataSource最后还会调用Java层的MediaPlayerBridge类的成员函数prepareAsync,目的是让Android系统提供的播放器就准备就绪播放。

       我们假设参数url描述的URL对应的视频在本地不存在,接下来Java层的MediaPlayerBridge类的成员函数setDataSource就会被调用,如下所示:

public class MediaPlayerBridge {
    ......

    @CalledByNative
    protected boolean setDataSource(
            Context context, String url, String cookies, String userAgent, boolean hideUrlLog) {
        Uri uri = Uri.parse(url);
        HashMap<String, String> headersMap = new HashMap<String, String>();
        ......
        if (!TextUtils.isEmpty(cookies)) headersMap.put("Cookie", cookies);
        if (!TextUtils.isEmpty(userAgent)) headersMap.put("User-Agent", userAgent);
        ......

        try {
            getLocalPlayer().setDataSource(context, uri, headersMap);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    ......
}
       这个函数定义在文件external/chromium_org/media/base/android/java/src/org/chromium/media/MediaPlayerBridge.java中。

       MediaPlayerBridge类的成员函数setDataSource首先将参数url描述的URL封装成一个URI对象,接着又将参数cookies和userAgent描述的Cookie和用户凭证保存一个Hash Map中。

       准备好上述视频源相关的信息后,MediaPlayerBridge类的成员函数setDataSource就调用成员函数getLocalPlayer获得一个由Android系统提供的播放器,如下所示:

public class MediaPlayerBridge {
    ......

    protected MediaPlayer getLocalPlayer() {
        if (mPlayer == null) {
            mPlayer = new MediaPlayer();
        }
        return mPlayer;
    }

    ......
}
       这个函数定义在文件external/chromium_org/media/base/android/java/src/org/chromium/media/MediaPlayerBridge.java中。

       从这里就可以看到,MediaPlayerBridge类的成员函数getLocalPlayer返回的是成员变量mPlayer指向的一个MediaPlayer对象。这个MediaPlayer对象描述的就是Android系统提供的播放器。如果这个播放器还没有创建,那么MediaPlayerBridge类的成员函数getLocalPlayer会先创建。

       回到MediaPlayerBridge类的成员函数setDataSource中,接下来它就会将前面准备好的视频源信息设置给Android系统提供的播放器,这是通过调用MediaPlayer类的成员函数setDataSource实现的。这是一个Android SDK接口,它的具体用法可以参考SDK文档。

       这一步执行完成后,回到C++层的MediaPlayerBridge类的成员函数Prepare中,它接下来继续调用Java层的MediaPlayerBridge类的成员函数prepareAsync让前面获得的由Android系统提供的播放器进入就绪状态,如下所示:

public class MediaPlayerBridge {
    ......
    @CalledByNative
    protected boolean prepareAsync() {
        try {
            getLocalPlayer().prepareAsync();
        } catch (IllegalStateException e) {
            Log.e(TAG, "Unable to prepare MediaPlayer.", e);
            return false;
        }
        return true;
    }

    ......
}

       这个函数定义在文件external/chromium_org/media/base/android/java/src/org/chromium/media/MediaPlayerBridge.java中。

       MediaPlayerBridge类的成员函数prepareAsync调用MediaPlayer类的成员函数prepareAsync让前面创建的由Android系统提供的播放器异步进入就绪状态。

       C++层的MediaPlayerBridge类有一个成员变量listener_,它指向C++层的一个MediaPlayerListener对象。调用C++层的MediaPlayerBridge类的成员函数SetMediaPlayerListener可以为这个MediaPlayerListener对象在Java层创建一个对应的MediaPlayerListener对象,如下所示:

void MediaPlayerBridge::SetMediaPlayerListener() {
  jobject j_context = base::android::GetApplicationContext();
  ......

  listener_->CreateMediaPlayerListener(j_context, j_media_player_bridge_.obj());
}
       这个函数定义在文件external/chromium_org/media/base/android/java/src/org/chromium/media/MediaPlayerBridge.java中。

       C++层的MediaPlayerBridge类的成员函数SetMediaPlayerListener通过调用C++层的MediaPlayerListener类的成员函数CreateMediaPlayerListener为其成员变量listener_创建一个Java层的MediaPlayerListener对象,如下所示:

void MediaPlayerListener::CreateMediaPlayerListener(
    jobject context, jobject media_player_bridge) {
  JNIEnv* env = AttachCurrentThread();
  ......
  j_media_player_listener_.Reset(
      Java_MediaPlayerListener_create(
          env, reinterpret_cast<intptr_t>(this), context, media_player_bridge));
}
       这个函数定义在文件external/chromium_org/media/base/android/media_player_listener.cc中。

       这个Java层的MediaPlayerListener对象是通过调用Java层的MediaPlayerListener类的静态成员函数create创建的,并且保存在C++层的MediaPlayerListener类的成员变量j_media_player_listener_中。

       Java层的MediaPlayerListener类的静态成员函数create的实现如下所示:

class MediaPlayerListener implements MediaPlayer.OnPreparedListener,
    ......

    // Used to determine the class instance to dispatch the native call to.
    private long mNativeMediaPlayerListener = 0;
    ......

    private MediaPlayerListener(long nativeMediaPlayerListener, Context context) {
        mNativeMediaPlayerListener = nativeMediaPlayerListener;
        ......
    }


    @CalledByNative
    private static MediaPlayerListener create(long nativeMediaPlayerListener,
            Context context, MediaPlayerBridge mediaPlayerBridge) {
        final MediaPlayerListener listener =
                new MediaPlayerListener(nativeMediaPlayerListener, context);
        ......
        mediaPlayerBridge.setOnPreparedListener(listener);
        ......

        return listener;
    }

    ......
}

       这个函数定义在文件external/chromium_org/media/base/android/java/src/org/chromium/media/MediaPlayerListener.java中。

       从前面的调用过程可以知道,参数nativeMediaPlayerListener描述的是C++层的一个MediaPlayerListener对象的地址。MediaPlayerListener类的静态成员函数create使用这个地址创建了一个Java层的MediaPlayerListener对象,并且将该地址保存创建出来的Java层的MediaPlayerListener对象的成员变量mNativeMediaPlayerListener中。通过这种方式,就使得C++层的MediaPlayerListener对象与Java层的MediaPlayerListener对象一一对应。

      参数mediaPlayerBridge指向的是一个Java层的MediaPlayerBridge对象。这个MediaPlayerBridge是我们在前面创建的,它内部包含了一个由Android系统提供的播放器。MediaPlayerListener类的静态成员函数create调用这个MediaPlayerBridge对象的成员函数setOnPreparedListener,用来监听它内部的播放器的准备就绪事件,如下所示:

public class MediaPlayerBridge {
    ......

    protected void setOnPreparedListener(MediaPlayer.OnPreparedListener listener) {
        getLocalPlayer().setOnPreparedListener(listener);
    }

    ......
}

       这个函数定义在文件external/chromium_org/media/base/android/java/src/org/chromium/media/MediaPlayerBridge.java中。

       一旦MediaPlayerBridge类内部维护的播放器进入准备就绪状态,参数listener指向的MediaPlayerListener对象就会获得通知,表现为它的成员函数onPrepared被调用,如下所示:

class MediaPlayerListener implements MediaPlayer.OnPreparedListener,
    MediaPlayer.OnCompletionListener,
    MediaPlayer.OnBufferingUpdateListener,
    MediaPlayer.OnSeekCompleteListener,
    MediaPlayer.OnVideoSizeChangedListener,
    MediaPlayer.OnErrorListener,
    AudioManager.OnAudioFocusChangeListener {
    ......

    @Override
    public void onPrepared(MediaPlayer mp) {
        nativeOnMediaPrepared(mNativeMediaPlayerListener);
    }

    ......
}
       这个函数定义在文件external/chromium_org/media/base/android/java/src/org/chromium/media/MediaPlayerListener.java中。

       MediaPlayerListener类的成员函数onPrepared通过调用另外一个成员函数nativeOnMediaPrepared通知成员变量mNativeMediaPlayerListener描述的一个C++层的MediaPlayerListener对象,前面在Java层创建的由Android系统提供的播放器已经进入准备就绪状态。

       MediaPlayerListener类的成员函数nativeOnMediaPrepared是一个JNI函数,它是由C++层的函数Java_com_android_org_chromium_media_MediaPlayerListener_nativeOnMediaPrepared实现,如下所示:

__attribute__((visibility("default")))
void
    Java_com_android_org_chromium_media_MediaPlayerListener_nativeOnMediaPrepared(JNIEnv*
    env,
    jobject jcaller,
    jlong nativeMediaPlayerListener) {
  MediaPlayerListener* native =
      reinterpret_cast<MediaPlayerListener*>(nativeMediaPlayerListener);
  CHECK_NATIVE_PTR(env, jcaller, native, "OnMediaPrepared");
  return native->OnMediaPrepared(env, jcaller);
}
       这个函数定义在文件out/target/product/generic/obj/GYP/shared_intermediates/media/jni/MediaPlayerListener_jni.h中。

       函数Java_com_android_org_chromium_media_MediaPlayerListener_nativeOnMediaPrepared首先将参数nativeMediaPlayerListener转换为一个C++层的MediaPlayerListener对象,然后再调用这个MediaPlayerListener对象的成员函数OnMediaPrepared,用来通知它前面在Java层创建的由Android系统提供的播放器已经进入准备就绪状态,如下所示:

void MediaPlayerListener::OnMediaPrepared(
    JNIEnv* /* env */, jobject /* obj */) {
  task_runner_->PostTask(FROM_HERE, base::Bind(
      &MediaPlayerBridge::OnMediaPrepared, media_player_));
}
       这个函数定义在文件external/chromium_org/media/base/android/media_player_listener.cc中。

       MediaPlayerListener类的成员变量task_runner_描述的是Browser进程主线程的消息队列,MediaPlayerListener的成员函数OnMediaPrepared向这个消息队列发送一个Task,这个Task绑定了MediaPlayerBridge类的成员函数OnMediaPrepared。这意味着接下来MediaPlayerBridge类的成员函数OnMediaPrepared会在Browser进程主线程被调用,如下所示:

void MediaPlayerBridge::OnMediaPrepared() {
  ......

  prepared_ = true;
  ......

  if (pending_play_) {
    StartInternal();
    pending_play_ = false;
  }

  ......
}
       这个函数定义在文件external/chromium_org/media/base/android/media_player_bridge.cc中。

       MediaPlayerBridge类的成员函数OnMediaPrepared首先将成员变量prepared_的值设置为true,表示当前正在处理的MediaPlayerBridge对象所描述的播放器已经准备就绪。

       MediaPlayerBridge类的成员函数OnMediaPrepared接下来还会检查另外一个成员变量pending_play_的值是否等于true。如果等于true,那么就说明当前正在处理的MediaPlayerBridge对象所描述的播放器正在等待被启动。这时候就可以调用MediaPlayerBridge类的另外一个成员函数StartInternal进行启动,如下所示:

void MediaPlayerBridge::StartInternal() {
  JNIEnv* env = base::android::AttachCurrentThread();
  Java_MediaPlayerBridge_start(env, j_media_player_bridge_.obj());
  ......
}
      这个函数定义在文件external/chromium_org/media/base/android/media_player_bridge.cc中。

      MediaPlayerBridge类的另外一个成员函数StartInternal调用Java层的MediaPlayerBridge类的成员函数start启动由Android系统提供的播放器,如下所示:

public class MediaPlayerBridge {
    ......

    @CalledByNative
    protected void start() {
        getLocalPlayer().start();
    }

    ......
}
       这个函数定义在文件external/chromium_org/media/base/android/java/src/org/chromium/media/MediaPlayerBridge.java中。

       MediaPlayerBridge类的成员函数start首先调用成员函数getLcoalPlayer获得一个MediaPlayer对象。这个MediaPlayer对象描述的即为由Android系统提供的播放器。有了这个MediaPlayer对象之后,MediaPlayerBridge类的成员函数start就调用它的成员函数start,这样就可以将它描述的播放器启动起来。

       至此,我们就分析完成了Chromium为视频标签<video>创建播放器的过程。这个过程一共创建了四个播放器相关的接口:

       1. WebMediaPlayerClientImpl

       2. WebMediaPlayerAndroid

       3. WebMediaBridgeBridge

       4. MediaPlayer

       其中,WebMediaPlayerClientImpl在WebKit中使用,它通过WebMediaPlayerAndroid实现播放器功能。WebMediaPlayerAndroid在Render进程中使用,它通过WebMediaBridgeBridge实现播放器功能。WebMediaBridgeBridge在Browser进程中使用,它通过MediaPlayer实现播放器功能。

       MediaPlayer是Android系统提供的播放器实现,这意味着在Chromium for Android上,网页中的<video>标签指定的视频是由Android系统提供的播放器进行播放的。在非全屏模式下,视频的内容需要作为网页的一部分进行显示。这意味着播放器需要将解码后得到的视频图像交回给Chromium,然后Chromium再将这些视频图像与网页的其它元素一起渲染到屏幕中去。在接下来一篇文章中,我们就详细分析这个渲染过程,敬请关注!更多的信息也可以关注老罗的新浪微博:http://weibo.com/shengyangluo

作者:Luoshengyang 发表于2016/8/15 0:59:33 原文链接
阅读:566 评论:2 查看评论

OpenGL学习(2)基本图形绘制

$
0
0

OpenGL学习(2)基本图形绘制

继续OpenGL学习(1)基本概念
进行绘制基本图形
只需修改GLRender

package com.android.chapter3;

import java.nio.IntBuffer;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import android.opengl.GLU;
import android.opengl.GLSurfaceView.Renderer;

public class GLRender implements Renderer
{
    int one = 0x10000;

    // 用于控制三角形和正方形旋转的角度
    float rotateTri, rotateQuad;

    //三角形三个顶点
     private IntBuffer triggerBuffer = IntBuffer.wrap(new int[]{
                   0, one,0,    //上顶点
                -one,-one,0,    //左下点
                 one,-one,0,}); //右下点

     //正方形的4个顶点
     private IntBuffer quaterBuffer = IntBuffer.wrap(new int[]{
                 one, one,0,
                -one, one,0,
                 one,-one,0,
                -one,-one,0});

     //三角形的顶点颜色值(r,g,b,a)
     private IntBuffer colorBuffer = IntBuffer.wrap(new int[]{
                one,  0,  0,one,
                  0,one,  0,one,
                  0,  0,one,one,
     });

    @Override
    public void onDrawFrame(GL10 gl)
    {
        // TODO Auto-generated method stub

        // 首先清理屏幕
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

        // 设置模型视图矩阵
        gl.glMatrixMode(GL10.GL_MODELVIEW);

        //重置矩阵
        gl.glLoadIdentity();

        // 视点变换
        GLU.gluLookAt(gl, 0, 0, 3, 0, 0, 0, 0, 1, 0);

        // 设置模型位置
        gl.glTranslatef(-3.0f, 0.0f, -4.0f);

        //设置旋转(y轴)
        gl.glRotatef(rotateTri, 0.0f, 1.0f, 0.0f);

        // 允许设置顶点
        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

        // 允许设置颜色数组
        gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

        //设置颜色数组
        gl.glColorPointer(4, GL10.GL_FIXED, 0, colorBuffer);

        // 设置三角形的顶点数据
        gl.glVertexPointer(3, GL10.GL_FIXED, 0, triggerBuffer);

        //放大三角形
        gl.glScalef(2.0f, 2.0f, 2.0f);

        //绘制三角形
        gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);

        //关闭颜色数组的设置
        gl.glDisableClientState(GL10.GL_COLOR_ARRAY);

        /*****正方形*****/

        //设置正方形的颜色
        gl.glColor4f(0.5f, 0.5f, 1.0f, 1.0f);

        // 重置当前的模型观察矩阵
        gl.glLoadIdentity();

        // 设置模型位置
        gl.glTranslatef(1.0f, 0.0f, -4.0f);

        //设置旋转(x轴)
        gl.glRotatef(rotateQuad, 1.0f, 0.0f, 0.0f);

        //设置正方形顶点数组
        gl.glVertexPointer(3, GL10.GL_FIXED, 0, quaterBuffer);

        //绘制正方形
        gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);

        /* 绘制出来则是线框 */
        //gl.glDrawArrays(GL10.GL_LINES, 0, 4);

        // 取消顶点设置
        gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);

        //改变旋转的角度
        rotateTri += 0.5f;
        rotateQuad -= 0.5f;

    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height)
    {
        // TODO Auto-generated method stub

        float ratio = (float) width / height;

        // 设置视口(OpenGL场景的大小)
        gl.glViewport(0, 0, width, height);

        // 设置投影矩阵为透视投影
        gl.glMatrixMode(GL10.GL_PROJECTION);

        // 重置投影矩阵(置为单位矩阵)
        gl.glLoadIdentity();

        //创建一个透视投影矩阵(设置视口大小)
        gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);

    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config)
    {
        // TODO Auto-generated method stub

        //告诉系统需要对透视进行修正
        gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);

        //设置清理屏幕的颜色
        gl.glClearColor(0, 0, 0, 1);

        //启用深度缓存
        gl.glEnable(GL10.GL_DEPTH_TEST);
    }

}

出现错误:

java.lang.IllegalArgumentException: Must use a native order direct Buffer异常。

最后发现不能通过这种方式创建顶点缓存数组。

修改之后的效果图:

package com.example.mychapter2;

import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import android.opengl.GLSurfaceView.Renderer;
import android.util.Log;

public class GLRender implements Renderer{  

    private String TAG = "GLRender";  
    float roateTri;//用于三角形的角度  
    float roateQuad;//用于四边形的角度  
    int one = 0x10000;  
    /*//三角形三个顶点 
    private IntBuffer triggerBuffer = IntBuffer.wrap(new int[]{ 
        0,one,0, 
        -one,-one,0, 
        one,-one,0, 
    }); 
    //四边形四个顶点 
    private IntBuffer quaterBuffer = IntBuffer.wrap(new int[]{ 
            -one,one,0, 
            one,one,0, 
            one,-one,0, 
            -one,-one,0, 
    });*/  
    int [] colorArray = {  
            one,0,0,one,  
            0,one,0,one,  
            0,0,one,one,  
    };  
    int [] triggerArray ={  
            0,one,0,  
            -one,-one,0,  
            one,-one,0};  
    int []  quaterArray = {  
            one,one,0,  
            -one,one,0,  
            one,-one,0,  
            -one,-one,0  
    };  

    @Override  
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {  
        // TODO Auto-generated method stub  
        Log.i(TAG, "onSurfaceCreated");  
        //告诉系统对透视进行修正,会使透视图看起来好看点  
        gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_FASTEST);  
        //黑色背景  
        gl.glClearColor(0, 0, 0, 0);//红,绿,蓝,apaha  
        //启动阴影平滑   
        gl.glShadeModel(GL10.GL_SMOOTH);  

        //设置深度缓存  
        gl.glClearDepthf(1.0f);  
        //启用深度测试  
        gl.glEnable(GL10.GL_DEPTH_TEST);  
        //所做深度测试的类型  
        gl.glDepthFunc(GL10.GL_LEQUAL);  
    }  

    @Override  
    public void onSurfaceChanged(GL10 gl, int width, int height) {  
        // TODO Auto-generated method stub  
        Log.i(TAG, "onSurfaceChanged width:"+width+" height:"+height);//1920 944  

        float radio = (float)width/height;  

        //设置OpenGL场景的大小  
        gl.glViewport(0, 0, width, height);  
        //设置投影矩阵,投影矩阵负责为场景增加透视  
        gl.glMatrixMode(GL10.GL_PROJECTION);  
        //重置投影矩阵  
        gl.glLoadIdentity();  
        //设置视口的大小 前四个参数去顶窗口的大小,分别是左,右,下,上,后两个参数分别是在场景中所能绘制深度的起点和终点  
        gl.glFrustumf(-radio, radio, -1, 1, 1, 10);  
        //指明任何新的变换即那个会影响 模型观察矩阵  
        gl.glMatrixMode(GL10.GL_MODELVIEW);  
        gl.glLoadIdentity();  

    }  

    @Override  
    public void onDrawFrame(GL10 gl) {  
        // TODO Auto-generated method stub  

        Log.i("GLRender", "onDrawFrame");  

        roateTri +=0.5f;  
        roateQuad-=0.5f;  

        //清除屏幕和深度缓存  
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT|GL10.GL_DEPTH_BUFFER_BIT);  
        // 重置当前的模型观察矩阵  
        gl.glLoadIdentity();  
        //移动当前中心点,左移1.5单位,并移入屏幕6.0,y不变  
        //注意:屏幕内移动的单位数必须小于前面我们通过  
        //glFrustumf方法所设置的最远距离,否则显示不出来。  
        //腰围OpenGL设置一个顶点数组,故需要告诉OpenGL要设置  
        //顶点这个功能。  
        //开启顶点设置功能  
        gl.glTranslatef(-1.5f, 0.0f, -6.0f);  

        //设置某无题沿着指定的轴旋转  
        //参数1:旋转的角度  
        //后三个参数共通决定旋转的方向  
        //注意:要在画图前,使用旋转  
        gl.glRotatef(roateTri, 0.0f, -1.0f, 0.0f);  

        //开启颜色渲染功能  
        gl.glEnableClientState(GL10.GL_COLOR_ARRAY);  
        //设置颜色,平滑着色  
        gl.glColorPointer(4, GL10.GL_FIXED, 0, bufferUtil(colorArray));  

        //允许设置顶点  
        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);  
        //设置三角形  
        //参数1:描述顶点的尺寸,本例中使用X,Y,Z坐标系,所以是3  
        //参数2:描述顶点的类型,本例中数据是固定的,所以使用了GL_FIXED表示固定顶点  
        //参数3:描述步长  
        //参数4:顶点缓存,即我们创建的顶点数组  
        gl.glVertexPointer(3, GL10.GL_FIXED, 0, bufferUtil(triggerArray));  
        //绘制三角形  
        //参数1:绘制模式,GL_TRIANGLES:表示绘制三角形  
        //参数2:开始位置  
        //参数3:要绘制的顶点计数  
        gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);  

        //重置当前的模型观察矩阵  
        gl.glLoadIdentity();  


        //关闭颜色渲染  
        gl.glDisableClientState(GL10.GL_COLOR_ARRAY);  

        //左移1.5单位,并移入屏幕6.0  
        gl.glTranslatef(1.5f, 0.0f, -6.0f);  

        gl.glRotatef(roateQuad, 1.0f, 0.0f, 0.0f);  

        //开启颜色渲染功能  
        gl.glEnableClientState(GL10.GL_COLOR_BUFFER_BIT);  
        //设置颜色,单调着色 (r,g,b,a)  
        gl.glColor4f(0.5f, 0.5f, 1.0f, 1.0f);  

        //设置和绘制正方形  
        gl.glVertexPointer(3, GL10.GL_FIXED, 0, bufferUtil(quaterArray));  
        gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);  

        //关闭颜色渲染  
        gl.glDisableClientState(GL10.GL_COLOR_BUFFER_BIT);  
        //取消顶点设置  
        gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);  

    }  
    /* 
     * OpenGL 是一个非常底层的画图接口,它所使用的缓冲区存储结构是和我们的 java 程序中不相同的。 
     * Java 是大端字节序(BigEdian),而 OpenGL 所需要的数据是小端字节序(LittleEdian)。 
     * 所以,我们在将 Java 的缓冲区转化为 OpenGL 可用的缓冲区时需要作一些工作。建立buff的方法如下 
     * */  
    public Buffer bufferUtil(int []arr){  
         IntBuffer mBuffer ;  

         //先初始化buffer,数组的长度*4,因为一个int占4个字节  
        ByteBuffer qbb = ByteBuffer.allocateDirect(arr.length * 4);  
        //数组排列用nativeOrder  
         qbb.order(ByteOrder.nativeOrder());  

         mBuffer = qbb.asIntBuffer();  
         mBuffer.put(arr);  
         mBuffer.position(0);  

         return mBuffer;  
    }  

}  
作者:wuxintdrh 发表于2016/8/15 1:23:48 原文链接
阅读:188 评论:0 查看评论

如何实现ButterKnife (二) —— BindResource

$
0
0

相关文章:

如何实现ButterKnife (一) —— 搭建开发框架

周末两天早起看TI,看中国夺冠还是很激动的,周末时间一晃也就过去了。不说废话了,接着上一篇现在从最简单的Resource资源绑定来说明,大体了解整个开发基本流程。

@BindString

先定义个用来注入字符串资源的注解:

/**
 * 字符串资源注解
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindString {
    @StringRes int value();
}

可以看到这是一个编译时注解(RetentionPolicy.CLASS),并且注解指定为Field注解(ElementType.FIELD),注解有个int型的属性值用来标注字符串资源ID(R.string.xxx)。注意这里对这个属性使用了 @StringRes 注解来限定取值范围只能是字符串资源,这个注解是在com.android.support:support-annotations 库里的,也就是之前为什么要加这个库。

再来看下注解处理器怎么处理这个注解:

@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {

    private Types typeUtils;
    private Elements elementUtils;
    private Filer filer;
    private Messager messager;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        typeUtils = processingEnv.getTypeUtils();
        elementUtils = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // 保存包含注解元素的目标类,注意是使用注解的外围类,主要用来处理父类继承,例:MainActivity
        Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
        // TypeElement 使用注解的外围类,BindingClass 对应一个要生成的类
        Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<>();

        // 处理BindString
        for (Element element : roundEnv.getElementsAnnotatedWith(BindString.class)) {
            if (VerifyHelper.verifyResString(element, messager)) {
                ParseHelper.parseResString(element, targetClassMap, erasedTargetNames, elementUtils);
            }
        }
        // 略...

        for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
            TypeElement typeElement = entry.getKey();
            BindingClass bindingClass = entry.getValue();

            // 查看是否父类也进行注解绑定,有则添加到BindingClass
            TypeElement parentType = _findParentType(typeElement, erasedTargetNames);
            if (parentType != null) {
                BindingClass parentBinding = targetClassMap.get(parentType);
                bindingClass.setParentBinding(parentBinding);
            }

            try {
                // 生成Java文件
                bindingClass.brewJava().writeTo(filer);
            } catch (IOException e) {
                _error(typeElement, "Unable to write view binder for type %s: %s", typeElement,
                        e.getMessage());
            }
        }
        return true;
    }

    /**
     * 查找父类型
     * @param typeElement   类元素
     * @param erasedTargetNames 存在的类元素
     * @return
     */
    private TypeElement _findParentType(TypeElement typeElement, Set<TypeElement> erasedTargetNames) {
        TypeMirror typeMirror;
        while (true) {
            // 父类型要通过 TypeMirror 来获取
            typeMirror = typeElement.getSuperclass();
            if (typeMirror.getKind() == TypeKind.NONE) {
                return null;
            }
            // 获取父类元素
            typeElement = (TypeElement) ((DeclaredType)typeMirror).asElement();
            if (erasedTargetNames.contains(typeElement)) {
                // 如果父类元素存在则返回
                return typeElement;
            }
        }
    }
}
代码上的注释都大概说明了所做的事,现在只看关键的几个地方:

1、erasedTargetNames 保存的是使用注解的外围类元素,如 MainActivity 里有个变量 String mBindString 使用了 @BindString,那这里就会保存 MainActivity所对应的元素。这个主要在后面处理父类绑定继承的问题上需要用到,暂时不介绍这个可以先了解下即可;

2、targetClassMap 的键值对存储的是键就是上面的外围类元素,而值是一个 BindingClass,它保存了我们要生成的Java代码所有必要的数据信息,一个 BindingClass对应一个生成的Java类;

3、VerifyHelper 和 ParseHelper是我另外封装的两个帮助类,主要是把注解处理的过程进行拆分,这样看逻辑思路的时候会清晰点,我们所做的处理工作大部分都在这里面进行,注意这样拆分不是必须的;

4、最后的代码就是生产Java文件的处理了,这个也放后面说明;

先来看下 VerifyHelper.verifyResString() 做了什么:

/**
 * 检验元素有效性帮助类
 */
public final class VerifyHelper {

    private static final String STRING_TYPE = "java.lang.String";

    private VerifyHelper() {
        throw new AssertionError("No instances.");
    }

    /**
     * 验证 String Resource
     */
    public static boolean verifyResString(Element element, Messager messager) {
        return _verifyElement(element, BindString.class, messager);
    }

    /**
     * 验证元素的有效性
     * @param element   注解元素
     * @param annotationClass   注解类
     * @param messager  提供注解处理器用来报告错误消息、警告和其他通知
     * @return  有效则返回true,否则false
     */
    private static boolean _verifyElement(Element element, Class<? extends Annotation> annotationClass,
                                        Messager messager) {
        // 检测元素的有效性
        if (!SuperficialValidation.validateElement(element)) {
            return false;
        }
        // 获取最里层的外围元素
        TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

        if (!_verifyElementType(element, annotationClass, messager)) {
            return false;
        }
        // 使用该注解的字段访问权限不能为 private 和 static
        Set<Modifier> modifiers = element.getModifiers();
        if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.STATIC)) {
            _error(messager, element, "@%s %s must not be private or static. (%s.%s)",
                    annotationClass.getSimpleName(), "fields", enclosingElement.getQualifiedName(),
                    element.getSimpleName());
            return false;
        }
        // 包含该注解的外围元素种类必须为 Class
        if (enclosingElement.getKind() != ElementKind.CLASS) {
            _error(messager, enclosingElement, "@%s %s may only be contained in classes. (%s.%s)",
                    annotationClass.getSimpleName(), "fields", enclosingElement.getQualifiedName(),
                    element.getSimpleName());
            return false;
        }
        // 包含该注解的外围元素访问权限不能为 private
        if (enclosingElement.getModifiers().contains(Modifier.PRIVATE)) {
            _error(messager, enclosingElement, "@%s %s may not be contained in private classes. (%s.%s)",
                    annotationClass.getSimpleName(), "fields", enclosingElement.getQualifiedName(),
                    element.getSimpleName());
            return false;
        }
        // 判断是否处于错误的包中
        String qualifiedName = enclosingElement.getQualifiedName().toString();
        if (qualifiedName.startsWith("android.")) {
            _error(messager, element, "@%s-annotated class incorrectly in Android framework package. (%s)",
                    annotationClass.getSimpleName(), qualifiedName);
            return false;
        }
        if (qualifiedName.startsWith("java.")) {
            _error(messager, element, "@%s-annotated class incorrectly in Java framework package. (%s)",
                    annotationClass.getSimpleName(), qualifiedName);
            return false;
        }

        return true;
    }

    /**
     * 验证元素类型的有效性
     * @param element   元素
     * @param annotationClass   注解类
     * @param messager  提供注解处理器用来报告错误消息、警告和其他通知
     * @return  有效则返回true,否则false
     */
    private static boolean _verifyElementType(Element element, Class<? extends Annotation> annotationClass,
                                              Messager messager) {
        // 获取最里层的外围元素
        TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

        // 检测使用该注解的元素类型是否正确
        if (annotationClass == BindString.class) {
            if (!STRING_TYPE.equals(element.asType().toString())) {
                _error(messager, element, "@%s field type must be 'String'. (%s.%s)",
                        annotationClass.getSimpleName(), enclosingElement.getQualifiedName(),
                        element.getSimpleName());
                return false;
            }
        } 
        // 略...

        return true;
    }

    /**
     * 输出错误信息
     * @param element
     * @param message
     * @param args
     */
    private static void _error(Messager messager, Element element, String message, Object... args) {
        if (args.length > 0) {
            message = String.format(message, args);
        }
        messager.printMessage(Diagnostic.Kind.ERROR, message, element);
    }
}

我把验证过程也分为几个方法处理,因为其它注解的处理过程也基本类似,只是在 _verifyElementType() 判断类型的时候会稍微有点不同,这个后面会看到。来整理下检测的流程,我也分步骤来说吧:

1、首先调用了 SuperficialValidation.validateElement(element) 来检测使用注解的元素的有效性,这个是 auto-common 提供的方法,可以看下官方说明;

2、_verifyElementType() 检测元素类型是否符合要求,因为我们在处理字符串资源的绑定,所以元素类型必须为 STRING_TYPE("java.lang.String")

3、然后就是判断外围元素种类必须为 Class 和一些访问权限的判断,因为我们后面肯定要对这些字段的值进行注入,所以需要有访问的权限;

4、最后就是判断外围元素不能处在 android. 和 java. 开头的系统包中,getQualifiedName() 取得是它完全限定名,即包含包路径的完整名称;

检测大体就这样,下面来看怎么解析注解:

/**
 * 注解解析绑定帮助类
 */
public final class ParseHelper {

    private ParseHelper() {
        throw new AssertionError("No instances.");
    }

    /**
     * 解析 String 资源
     *
     * @param element        使用注解的元素
     * @param targetClassMap 映射表
     * @param elementUtils   元素工具类
     */
    public static void parseResString(Element element, Map<TypeElement, BindingClass> targetClassMap,
                                      Set<TypeElement> erasedTargetNames, Elements elementUtils) {
        // 获取字段名和注解的资源ID
        String name = element.getSimpleName().toString();
        int resId = element.getAnnotation(BindString.class).value();

        BindingClass bindingClass = _getOrCreateTargetClass(element, targetClassMap, elementUtils);
        // 生成资源信息
        FieldResourceBinding binding = new FieldResourceBinding(resId, name, "getString");
        // 给BindingClass添加资源信息
        bindingClass.addResourceBinding(binding);

        erasedTargetNames.add((TypeElement) element.getEnclosingElement());
    }

    /**
     * 获取存在的 BindingClass,没有则重新生成
     *
     * @param element        使用注解的元素
     * @param targetClassMap 映射表
     * @param elementUtils   元素工具类
     * @return BindingClass
     */
    private static BindingClass _getOrCreateTargetClass(Element element, Map<TypeElement, BindingClass> targetClassMap,
                                                        Elements elementUtils) {
        TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
        BindingClass bindingClass = targetClassMap.get(enclosingElement);
        // 以下以 com.butterknife.MainActivity 这个类为例
        if (bindingClass == null) {
            // 获取元素的完全限定名称:com.butterknife.MainActivity
            String targetType = enclosingElement.getQualifiedName().toString();
            // 获取元素所在包名:com.butterknife
            String classPackage = elementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();
            // 获取要生成的Class的名称:MainActivity$$ViewBinder
            int packageLen = classPackage.length() + 1;
            String className = targetType.substring(packageLen).replace('.', '$') + BINDING_CLASS_SUFFIX;
            // 生成Class的完全限定名称:com.butterknife.MainActivity$$ViewBinder
            String classFqcn = classPackage + "." + className;

            /* 不要用下面这个来生成Class名称,内部类会出错,比如ViewHolder */
//            String className = enclosingElement.getSimpleName() + BINDING_CLASS_SUFFIX;

            bindingClass = new BindingClass(classPackage, className, targetType, classFqcn);
            targetClassMap.put(enclosingElement, bindingClass);
        }
        return bindingClass;
    }
}

可以看到这里主要就是获取对应的注解字段和它的注解属性值,然后生成一个 FieldResourceBinding 对象并添加到BindingClass中。我们前面介绍了一个外围类对应一个 BindingClass,而外围类可能同时包含多个注解的,所以 BindingClass可能是存在并保存在 targetClassMap中,这时我们直接去获取就行了。现在只要了解 FieldResourceBinding和 BindingClass的用法整个流程就通了,先来看下 FieldResourceBinding:

/**
 * 资源信息
 */
public final class FieldResourceBinding {

    // 资源ID
    private final int id;
    // 字段变量名称
    private final String name;  
    // 获取资源数据的方法
    private final String method;    

    public FieldResourceBinding(int id, String name, String method) {
        this.id = id;
        this.name = name;
        this.method = method;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getMethod() {
        return method;
    }
}

这个还是好理解,id name 都好理解,前面我们也通过注解获取了,至于 method我们现在取的是String资源所以传入"getString",这个等下在 BindingClass中就会用到。下面来看 BindingClass的实现:

/**
 * 绑定处理类,一个 BindingClass 对应一个要生成的类
 */
public final class BindingClass {

    private static final ClassName FINDER = ClassName.get("com.dl7.butterknifelib", "Finder");
    private static final ClassName VIEW_BINDER = ClassName.get("com.dl7.butterknifelib", "ViewBinder");
    private static final ClassName CONTEXT = ClassName.get("android.content", "Context");
    private static final ClassName RESOURCES = ClassName.get("android.content.res", "Resources");

    private final List<FieldResourceBinding> resourceBindings = new ArrayList<>();
    private final String classPackage;
    private final String className;
    private final String targetClass;
    private final String classFqcn;


    /**
     * 绑定处理类
     *
     * @param classPackage 包名:com.butterknife
     * @param className    生成的类:MainActivity$$ViewBinder
     * @param targetClass  目标类:com.butterknife.MainActivity
     * @param classFqcn    生成Class的完全限定名称:com.butterknife.MainActivity$$ViewBinder
     */
    public BindingClass(String classPackage, String className, String targetClass, String classFqcn) {
        this.classPackage = classPackage;
        this.className = className;
        this.targetClass = targetClass;
        this.classFqcn = classFqcn;
    }

    /**
     * 生成Java类
     *
     * @return JavaFile
     */
    public JavaFile brewJava() {
        // 构建一个类
        TypeSpec.Builder result = TypeSpec.classBuilder(className)
                .addModifiers(Modifier.PUBLIC)
                .addTypeVariable(TypeVariableName.get("T", ClassName.bestGuess(targetClass)));

        if (_hasParentBinding()) {
            // 有父类则继承父类
            result.superclass(ParameterizedTypeName.get(ClassName.bestGuess(parentBinding.classFqcn),
                    TypeVariableName.get("T")));
        } else {
            // 实现 ViewBinder 接口
            result.addSuperinterface(ParameterizedTypeName.get(VIEW_BINDER, TypeVariableName.get("T")));
        }

        // 添加方法
        result.addMethod(_createBindMethod());
        // 构建Java文件
        return JavaFile.builder(classPackage, result.build())
                .addFileComment("Generated code from Butter Knife. Do not modify!")
                .build();
    }

    /**
     * 创建方法
     *
     * @return MethodSpec
     */
    private MethodSpec _createBindMethod() {
        // 定义一个方法,其实就是实现 ViewBinder 的 bind 方法
        MethodSpec.Builder result = MethodSpec.methodBuilder("bind")
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC)
                .addParameter(FINDER, "finder", Modifier.FINAL)
                .addParameter(TypeVariableName.get("T"), "target", Modifier.FINAL)
                .addParameter(Object.class, "source");

        if (_hasParentBinding()) {
            // 调用父类的bind()方法
            result.addStatement("super.bind(finder, target, source)");
        }
        
        if (_hasResourceBinding()) {
            // 过滤警告
            result.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
                    .addMember("value", "$S", "ResourceType")
                    .build());

            result.addStatement("$T context = finder.getContext(source)", CONTEXT);
            result.addStatement("$T res = context.getResources()", RESOURCES);
            // Resource
            for (FieldResourceBinding binding : resourceBindings) {
                result.addStatement("target.$L = res.$L($L)", binding.getName(), binding.getMethod(),
                        binding.getId());
            }
        }

        return result.build();
    }

    /**
     * 添加资源
     *
     * @param binding 资源信息
     */
    public void addResourceBinding(FieldResourceBinding binding) {
        resourceBindings.add(binding);
    }

    private boolean _hasResourceBinding() {
        return !(resourceBindings.isEmpty() && colorBindings.isEmpty());
    }
}
这里只列了必要的步骤,其实整个过程就是用 javapoet 生成Java类,和我们注解相关的最主要是这句话:

result.addStatement("target.$L = res.$L($L)", binding.getName(), binding.getMethod(), binding.getId());

我们调用对应的方法(getMethod)并传入对应的ID(getId)来设置对应的字段(getName),和我们平常写句子的逻辑是一样的。关于javapoet的详细用法可以参考官方示例。

在处理的过程中我们还进行了父类继承的处理,为什么要进行这个处理呢?举个简单的例子,我们有一个类B,它有直接父类A,它们都有使用我们定义的注解,正常我们类B是可以调用类A中的非私有域,所以我们在执行类B的bind()方法时要先执行父类的bind()方法,即super.bind()

到这里整个流程就完成了,你再回头看看注解处理器最后生成Java文件的代码应该能理解是怎么回事了,现在在代码中应该也能正常使用了,使用代码我就不贴了,看下文章最后给的例子源码就行了。

@BindColor

接下来看下 @BindColor 注解怎么处理,其实大体流程都是一样的,先定义个注解:

/**
 * 绑定颜色资源
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindColor {
    @ColorRes int value();
}

同样的,这边用了 @ColorRes 来限定注解的属性值只能为 R.color.xxx

再看下注解处理器:

@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {
    
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    	// 略...	

		// 处理BindColor
	    for (Element element : roundEnv.getElementsAnnotatedWith(BindColor.class)) {
	        if (VerifyHelper.verifyResColor(element, messager)) {
	            ParseHelper.parseResColor(element, targetClassMap, erasedTargetNames, elementUtils);
	        }
	    }
	    // 略...
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(BindString.class.getCanonicalName());
        annotations.add(BindColor.class.getCanonicalName());
        return annotations;
    }
}

和刚才的注解处理基本一致,主要来看下怎么检查和解析注解,注意要在 getSupportedAnnotationTypes() 中指明要处理这个注解。检测如下:

public final class VerifyHelper {

    private static final String COLOR_STATE_LIST_TYPE = "android.content.res.ColorStateList";
    
    /**
     * 验证 Color Resource
     */
    public static boolean verifyResColor(Element element, Messager messager) {
        return _verifyElement(element, BindColor.class, messager);
    }

    private static boolean _verifyElement(Element element, Class<? extends Annotation> annotationClass,
                                        Messager messager) {
    	// 和 @BindString 一致
    }

    private static boolean _verifyElementType(Element element, Class<? extends Annotation> annotationClass,
                                              Messager messager) {
        // 获取最里层的外围元素
        TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
		if (annotationClass == BindColor.class) {
            if (COLOR_STATE_LIST_TYPE.equals(element.asType().toString())) {
                return true;
            } else if (element.asType().getKind() != TypeKind.INT) {
                _error(messager, element, "@%s field type must be 'int' or 'ColorStateList'. (%s.%s)",
                        BindColor.class.getSimpleName(), enclosingElement.getQualifiedName(),
                        element.getSimpleName());
                return false;
            }
        }

        return true;
    }
}

检测主要看元素类型的检测,其它的和上一个注解一致,在这里我们定义的注解可能会返回两种情况:一种是我们正常使用的颜色资源,返回 int 类型;还有一种是根据状态变化的颜色选择器,返回 COLOR_STATE_LIST_TYPE = "android.content.res.ColorStateList",由 <selector> 标签定义的一组颜色值。

下面看对注解的解析:

public final class ParseHelper {

    /**
     * 解析 String 资源
     *
     * @param element        使用注解的元素
     * @param targetClassMap 映射表
     * @param elementUtils   元素工具类
     */
    public static void parseResColor(Element element, Map<TypeElement, BindingClass> targetClassMap,
                                     Set<TypeElement> erasedTargetNames, Elements elementUtils) {
        // 获取字段名和注解的资源ID
        String name = element.getSimpleName().toString();
        int resId = element.getAnnotation(BindColor.class).value();

        BindingClass bindingClass = _getOrCreateTargetClass(element, targetClassMap, elementUtils);
        // 生成资源信息
        FieldColorBinding binding;
        if (COLOR_STATE_LIST_TYPE.equals(element.asType().toString())) {
            binding = new FieldColorBinding(resId, name, "getColorStateList");
        } else {
            binding = new FieldColorBinding(resId, name, "getColor");
        }
        // 给BindingClass添加资源信息
        bindingClass.addColorBinding(binding);

        erasedTargetNames.add((TypeElement) element.getEnclosingElement());
    }
}

还是和 @BindString 基本一致,主要多了对 COLOR_STATE_LIST_TYPE 的处理,这里分别设置不同的 Method 来获取数据。这边的 FieldColorBinding 和 FieldResourceBinding 的字段其实都是一样的,我把它们分开是后面生成Java文件的时候好判断,定义如下:

/**
 * 资源 Color 绑定信息
 */
public final class FieldColorBinding {

    private final int id;
    private final String name;
    private final String method;

    public FieldColorBinding(int id, String name, String method) {
        this.id = id;
        this.name = name;
        this.method = method;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getMethod() {
        return method;
    }
}

最后看下 BindingClass 的处理,只给出变化的部分:

public final class BindingClass {

    private static final ClassName CONTEXT = ClassName.get("android.content", "Context");
    private static final ClassName RESOURCES = ClassName.get("android.content.res", "Resources");
    private static final ClassName CONTEXT_COMPAT = ClassName.get("android.support.v4.content", "ContextCompat");

    private final List<FieldColorBinding> colorBindings = new ArrayList<>();

    private MethodSpec _createBindMethod() {
        // 略...
        if (_hasResourceBinding()) {
        	// 略...

            // ClassResource
            for (FieldColorBinding binding : colorBindings) {
                result.addStatement("target.$L = $T.$L(context, $L)", binding.getName(), CONTEXT_COMPAT,
                        binding.getMethod(), binding.getId());
            }
        }
    }

    public void addColorBinding(FieldColorBinding binding) {
        colorBindings.add(binding);
    }
}
可以看到在获取颜色的时候用了 v4 包中的 ContextCompat 类来处理,这是一个兼容类,因为在 SDK≥23 的时候获取颜色会需要传入一个 Resources.Theme,用兼容包的话会统一帮我们处理,这个和源码处理不一样,但效果是相似的。

到这里 @BindColor 也可以使用了,最后在代码中使用如下:

public class MainActivity extends AppCompatActivity {

    @BindString(R.string.activity_string)
    String mBindString;
    @BindColor(R.color.colorAccent)
    int mBindColor;
    @BindColor(R.color.sel_btn_text)
    ColorStateList mBtnTextColor;

    // 略...
}

看下注解处理器帮我们生成的代码:

public class MainActivity$$ViewBinder<T extends MainActivity> implements ViewBinder<T> {
    @Override
    @SuppressWarnings("ResourceType")
    public void bind(final Finder finder, final T target, Object source) {
        Context context = finder.getContext(source);
        Resources res = context.getResources();
        target.mBindString = res.getString(2131099669);
        target.mBindColor = ContextCompat.getColor(context, 2131427346);
        target.mBtnTextColor = ContextCompat.getColorStateList(context, 2131427399);
    }
}

可以看到应该是正常的,根据这些代码你再回想一下关于检测的权限、代码的生成过程应该有一个更好的认识。

关于其它资源绑定注解其实实现都和这两个类似,这方面我就不再说明了,下一个就到了最常用的 @Bind 绑定 View 视图的处理。

例子代码:ButterKnifeStudy


作者:github_35180164 发表于2016/8/15 9:33:53 原文链接
阅读:263 评论:0 查看评论

《Motion Design for iOS》(四十二)

$
0
0

构建立即响应的按钮

你玩过Loren Brichter的游戏Letterpress吗?我很喜欢的Loren构建的一个关于界面的东西可能不是每个人都明显喜欢的:我喜欢每个按钮在用户按下时立即切换到一个不同的状态的样子。绝对不会延迟。这不是一个简单实现的行为,因为即使你可以将一个图片设为UIButtonUIControlStateHighlighted状态图,它也只会在点击发生后一小会启动,而且它不允许更进一步的代码来运行它。如果我想要在用户点击一个UIButton后立即运行一个动画,我就不得不自己写一个简单的自定义按钮类。但首先,先来看一看我们要构建的是什么。



如果我想要在用户点击后立即运行代码,我就不得不自己写一个好的UIButton子类,这样我就可以重写一些方法,即 -touchesBegan:withEvent: 和 -touchesEnded:withEvent:。iOS中的每个界面的控制都从UIResponder继承了这些方法,它是一个处理所有触摸控制事件的父类。有了子类,我就可以塞一些自己的代码来在这些方法启动的时候运行。来看看DTCTestButton的实现文件,这是我们的按钮子类,会为我们处理一些魔法。

#import "DTCTestButton.h"
#import "POP.h"

@implementation DTCTestButton

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    // 自定义一些按钮第一次被点击时要运行的代码

    [super touchesBegan:touches withEvent:event];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    // 自定义一些按钮不再被点击时要运行的代码

    [super touchesEnded:touches withEvent:event];
}

@end

我们这里只定义了两个方法,我们想要将我们的代码放到这些方法里面去。当子类化一个苹果提供的对象,比如UIButton时,做一个好的城市居民并确保调用super的关于这些方法的实现是很重要的,因为我们不知道苹果在这两个方法中需要运行什么代码,而且不想破坏按钮的默认行为。我们调用super后,就可以在这两个方法中添加任何我们想要的行为。

让我们添加一个Pop动画到 -touchesBegan:withEvent:中去。

POPSpringAnimation *scale = [self pop_animationForKey:@"scale"];

if (scale) {
    scale.toValue = [NSValue valueWithCGPoint:CGPointMake(0.8, 0.8)];
} else {
    scale = [POPSpringAnimation animationWithPropertyNamed:kPOPViewScaleXY];
    scale.toValue = [NSValue valueWithCGPoint:CGPointMake(0.8, 0.8)];
    scale.springBounciness = 20;
    scale.springSpeed = 18.0f;
    [self pop_addAnimation:scale forKey:@"scale"];
}

这和我们之前写的Pop代码有点不同。当使用Pop来构建好的响应动画去关联触摸动作时,一个聪明的做法是看看是否已经有一个Pop动画关联到这个视图或者layer了。如果有,只要更新已经存在的动画的toValue属性就可以了。Pop知道当前的值是什么并且已经设置好弹性和速度变量了,所以你不用做任何其他的事情。这避免了添加另一个错误的Pop动画来操作同样的值(在这个例子中,是kPOPViewScaleXY),这会造成愚蠢的结果。通过使用现存的动画,Pop可以优雅地从它的当前位置修改到你设置的新的toValue并进行一个漂亮、平滑的过度。这也是为什么Pop动画有一个名字:这样你就可以通过给出你之前设置的动画的名字来询问视图或者layer它们是否有已经添加进去的Pop动画并获取到动画对象。

如果动画不是已经存在,我们就和平常一样创建一个新的Pop动画对象,设置弹簧的动作属性,比如弹性,设置toValue,然后添加动画到视图或者layer上。在这个例子中,我们动画了视图的尺寸,所以我们将动画添加到视图上。

现在让我们在触摸事件结束时做同样的事情。这次代码放在 -touchesEnded:withEvent:中。

POPSpringAnimation *scale = [self pop_animationForKey:@"scale"];

if (scale) {
    scale.toValue = [NSValue valueWithCGPoint:CGPointMake(1.0, 1.0)];
} else {
    scale = [POPSpringAnimation animationWithPropertyNamed:kPOPViewScaleXY];
    scale.toValue = [NSValue valueWithCGPoint:CGPointMake(1.0, 1.0)];
    scale.springBounciness = 20;
    scale.springSpeed = 18.0f;
    [self pop_addAnimation:scale forKey:@"scale"];
}

如果你看看触摸事件开始时0.8的toValue以及触摸结束时的1.0的toValue,你就可以猜到整个动画会在用户点击按钮时稍微收缩按钮的尺寸,然后会在他们停止触摸时弹回完整的尺寸。完全正确!这里是它现在的样子。



很有意思!让我们再加一点点旋转动画来增色。它基本上和我们已经添加的代码一样,只是重复它,修改动画类型,然后改变toValue值。这里是完整的代码,以及一些注释。

// 当用户开始点击时立即调用
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {    
    // 看动画是否已经被添加到视图或者layer上
    POPSpringAnimation *scale = [self pop_animationForKey:@"scale"];
    POPSpringAnimation *rotate = [self.layer pop_animationForKey:@"rotate"];

    // 如果scale动画已经存在,就设置toValue
    if (scale) {
        scale.toValue = [NSValue valueWithCGPoint:CGPointMake(0.8, 0.8)];
    } else {
        // 如果不存在,就创建并添加它
        scale = [POPSpringAnimation animationWithPropertyNamed:kPOPViewScaleXY];
        scale.toValue = [NSValue valueWithCGPoint:CGPointMake(0.8, 0.8)];
        scale.springBounciness = 20;
        scale.springSpeed = 18.0f;
        [self pop_addAnimation:scale forKey:@"scale"];
    }

    // 如果旋转动画已经存在,就设置toValue
    if (rotate) {
        rotate.toValue = @(M_PI/6); // 旋转到1/6th π角度
    } else {
        // 旋转动画时layer上的,所以我们添加到layer上去
        rotate = [POPSpringAnimation animationWithPropertyNamed:kPOPLayerRotation];
        rotate.toValue = @(M_PI/6);
        rotate.springBounciness = 20;
        rotate.springSpeed = 18.0f;

        // 添加到layer上,而不是view
        [self.layer pop_addAnimation:rotate forKey:@"rotate"];
    }

    [super touchesBegan:touches withEvent:event];
}

// 在用户离开手指时立即调用
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    // 看动画是否存在(由于这是用户离开时,基本是已经存在的)
    POPSpringAnimation *scale = [self pop_animationForKey:@"scale"];
    POPSpringAnimation *rotate = [self pop_animationForKey:@"rotate"];

    if (scale) {
        // 拉伸回1.0的完整尺寸
        scale.toValue = [NSValue valueWithCGPoint:CGPointMake(1.0, 1.0)];
    } else {
        scale = [POPSpringAnimation animationWithPropertyNamed:kPOPViewScaleXY];
        scale.toValue = [NSValue valueWithCGPoint:CGPointMake(1.0, 1.0)];
        scale.springBounciness = 20;
        scale.springSpeed = 18.0f;
        [self pop_addAnimation:scale forKey:@"scale"];
    }

    if (rotate) {
        // 旋转回0角度的初始位置
        rotate.toValue = @(0);
    } else {
        rotate = [POPSpringAnimation animationWithPropertyNamed:kPOPLayerRotation];
        rotate.toValue = @(0);
        rotate.springBounciness = 20;
        rotate.springSpeed = 18.0f;

        // 再次确保添加你的layer动画到layer上去。我曾经失误过很多次,这会导致一个有趣的bug :)
        [self.layer pop_addAnimation:rotate forKey:@"rotate"];
    }

    [super touchesEnded:touches withEvent:event];
}

动画代码是重复的。简单,但是重复。它的一个缺点是需要很多行代码来完整构建你的动画,但优点是能让你练习写很多动画代码,所以我认为你可以学的更快。

再一次,这里是我们构建的最终动画。它是一个很有趣的效果,会在用户点击按钮时立即启动,它会让你的界面感觉响应很快。这里的弹性效果很显著,所以当添加动画到你的真实app界面时,去使用一会app的动画,并确保它们的速度和动作时合适且不分散注意力的。



现在让我们来用Pop做一些有趣的东西!


查看完整合集:https://github.com/Cloudox/Motion-Design-for-iOS
版权所有:http://blog.csdn.net/cloudox_

作者:Cloudox_ 发表于2016/8/15 10:16:37 原文链接
阅读:158 评论:0 查看评论

Android--intent详解

$
0
0

【正文】

Intent组件虽然不是四大组件,但却是连接四大组件的桥梁,学习好这个知识,也非常的重要。

一、什么是Intent

1、Intent的概念:

  • Android中提供了Intent机制来协助应用间的交互与通讯,或者采用更准确的说法是,Intent不仅可用于应用程序之间,也可用于应用程序内部的activity, service和broadcast receiver之间的交互。Intent这个英语单词的本意是“目的、意向、意图”。
  • Intent是一种运行时绑定(runtime binding)机制,它能在程序运行的过程中连接两个不同的组件。通过Intent,你的程序可以向Android表达某种请求或者意愿,Android会根据意愿的内容选择适当的组件来响应。

activity、service和broadcast receiver之间是通过Intent进行通信的,而另外一个组件Content Provider本身就是一种通信机制,不需要通过Intent。我们来看下面这个图就知道了:

如果Activity1需要和Activity2进行联系,二者不需要直接联系,而是通过Intent作为桥梁。通俗来讲,Intnet类似于中介、媒婆的角色。

 

2、对于向这三种组件发送intent有不同的机制:

  • 使用Context.startActivity() 或 Activity.startActivityForResult(),传入一个intent来启动一个activity。使用 Activity.setResult(),传入一个intent来从activity中返回结果。
  • 将intent对象传给Context.startService()来启动一个service或者传消息给一个运行的service。将intent对象传给 Context.bindService()来绑定一个service。
  • 将intent对象传给 Context.sendBroadcast(),Context.sendOrderedBroadcast(),或者Context.sendStickyBroadcast()等广播方法,则它们被传给 broadcast receiver。

二、Intent的相关属性:

  • Intent由以下各个组成部分:
  • component(组件):目的组件
  • action(动作):用来表现意图的行动
  • category(类别):用来表现动作的类别
  • data(数据):表示与动作要操纵的数据
  • type(数据类型):对于data范例的描写
  • extras(扩展信息):扩展信息
  • Flags(标志位):期望这个意图的运行模式

Intent类型分为显式Intent(直接类型)、隐式Intent(间接类型)。官方建议使用隐式Intent。上述属性中,component属性为直接类型,其他均为间接类型。

相比与显式Intent,隐式Intnet则含蓄了许多,它并不明确指出我们想要启动哪一个活动,而是指定一系列更为抽象的action和category等信息,然后交由系统去分析这个Intent,并帮我们找出合适的活动去启动。

Activity 中 Intent Filter 的匹配过程 :

 

1、component(组件):目的组件

Component属性明确指定Intent的目标组件的类名称。(属于直接Intent)

如果 component这个属性有指定的话,将直接使用它指定的组件。指定了这个属性以后,Intent的其它所有属性都是可选的。

例如,启动第二个Activity时,我们可以这样来写:

复制代码
复制代码
 1         button1.setOnClickListener(new OnClickListener() {            
 2             @Override
 3             public void onClick(View v) {
 4                 //创建一个意图对象
 5                 Intent intent = new Intent();
 6                 //创建组件,通过组件来响应
 7                 ComponentName component = new ComponentName(MainActivity.this, SecondActivity.class);
 8                 intent.setComponent(component);                
 9                 startActivity(intent);                
10             }
11         });
复制代码
复制代码

如果写的简单一点,监听事件onClick()方法里可以这样写:

1                 Intent intent = new Intent();
2                 //setClass函数的第一个参数是一个Context对象
3                 //Context是一个类,Activity是Context类的子类,也就是说,所有的Activity对象,都可以向上转型为Context对象
4                 //setClass函数的第二个参数是一个Class对象,在当前场景下,应该传入需要被启动的Activity类的class对象
5                 intent.setClass(MainActivity.this, SecondActivity.class);
6                 startActivity(intent);    

再简单一点,可以这样写:(当然,也是最常见的写法)

1                 Intent intent = new Intent(MainActivity.this,SecondActivity.class);
2                 startActivity(intent);

 

 

2、Action(动作):用来表现意图的行动

当日常生活中,描述一个意愿或愿望的时候,总是有一个动词在其中。比如:我想“做”三个俯卧撑;我要“写” 一封情书,等等。在Intent中,Action就是描述做、写等动作的,当你指明了一个Action,执行者就会依照这个动作的指示,接受相关输入,表现对应行为,产生符合的输出。在Intent类中,定义了一批量的动作,比如ACTION_VIEW,ACTION_PICK等, 基本涵盖了常用动作。加的动作越多,越精确。

Action 是一个用户定义的字符串,用于描述一个 Android 应用程序组件,一个 Intent Filter 可以包含多个 Action。在 AndroidManifest.xml 的Activity 定义时,可以在其 <intent-filter >节点指定一个 Action列表用于标识 Activity 所能接受的“动作”。

 

3、category(类别):用来表现动作的类别

Category属性也是作为<intent-filter>子元素来声明的。例如:

<intent-filter>

  <action android:name="com.vince.intent.MY_ACTION"></action>

  <category android:name="com.vince.intent.MY_CATEGORY"></category> 

  <category android:name="android.intent.category.DEFAULT"></category> 

</intent-filter>   

Action 和category通常是放在一起用的,所以这里一起介绍一下。我们来先来举一个例子:

新建一个工程文件smyh006_Intent01,在默认文件的基础之上,新建文件SecondActicity.java和activity_second.xml。

紧接着,我们要到清单文件中进行注册,打开AndroidManifest.xml,添加SecondActivity的action和category的过滤器:

复制代码
复制代码
1         <activity 
2             android:name=".SecondActivity">
3             <intent-filter>
4                  <action android:name="com.example.smyh006intent01.MY_ACTION"/>
5                  <category android:name="android.intent.category.DEFAULT" />
6             </intent-filter>            
7         </activity>
复制代码
复制代码

上方代码,表示SecondActicity可以匹配第4行的MY_ACTION这个动作,此时,如果在其他的Acticity通过这个action的条件来查找,那SecondActicity就具备了这个条件。类似于相亲时,我要求对方有哪些条件,然后对方这个SecondActicity恰巧满足了这个条件(够通俗了吧)。

注:如果没有指定的category,则必须使用默认的DEFAULT(即上方第5行代码)。

也就是说:只有<action>和<category>中的内容同时能够匹配上Intent中指定的action和category时,这个活动才能响应Intent。如果使用的是DEFAULT这种默认的category,在稍后调用startActivity()方法的时候会自动将这个category添加到Intent中。

现在来修改MainActivity.java中按钮的点击事件,代码如下:

复制代码
复制代码
 1         button1.setOnClickListener(new OnClickListener() {            
 2             @Override
 3             public void onClick(View v) {
 4                 //启动另一个Activity,(通过action属性进行查找)
 5                 Intent intent = new Intent();
 6                 //设置动作(实际action属性就是一个字符串标记而已)
 7                 intent.setAction("com.example.smyh006intent01.MY_ACTION"); //方法:Intent android.content.Intent.setAction(String action)
 8                 startActivity(intent);        
 9             }
10         });
复制代码
复制代码

上方代码中,也可以换成下面这种简洁的方式:

复制代码
复制代码
1         button1.setOnClickListener(new OnClickListener() {            
2             @Override
3             public void onClick(View v) {
4                 //启动另一个Activity,(通过action属性进行查找)
5                 Intent intent = new Intent("com.example.smyh006intent01.MY_ACTION");//方法: android.content.Intent.Intent(String action)                
6                 startActivity(intent);        
7             }
8         });
复制代码
复制代码

上方第5行代码:在这个Intent中,我并没有指定具体哪一个Activity,我只是指定了一个action的常量。所以说,隐式Intent的作用就表现的淋漓尽致了。此时,点击MainActicity中的按钮,就会跳到SecondActicity中去。

上述情况只有SecondActicity匹配成功。如果有多个组件匹配成功,就会以对话框列表的方式让用户进行选择。我们来详细介绍一下:

我们新建文件ThirdActicity.java和activity_third.xml,然后在清单文件AndroidManifest.xml中添加ThirdActivity的action和category的过滤器:

复制代码
复制代码
1         <activity 
2             android:name=".ThirdActivity">
3             <intent-filter>
4                  <action android:name="com.example.smyh006intent01.MY_ACTION"/>
5                  <category android:name="android.intent.category.DEFAULT" />
6             </intent-filter>            
7         </activity> 
复制代码
复制代码

此时,运行程序,当点击MainActivity中的按钮时,弹出如下界面:

相信大家看到了这个界面,应该就一目了然了。于是我们可以做出如下总结:

在自定义动作时,使用activity组件时,必须添加一个默认的类别

具体的实现为:

<intent-filter>

               <action android:name="com.example.action.MY_ACTION"/>

               <category android:name="android.intent.category.DEFAULT"/>

</intent-filter>

如果有多个组件被匹配成功,就会以对话框列表的方式让用户进行选择。

每个Intent中只能指定一个action,但却能指定多个category;类别越多,动作越具体,意图越明确(类似于相亲时,给对方提了很多要求)。

目前我们的Intent中只有一个默认的category,现在可以通过intent.addCategory()方法来实现。修改MainActivity中按钮的点击事件,代码如下:

复制代码
复制代码
 1         button1.setOnClickListener(new OnClickListener() {            
 2             @Override
 3             public void onClick(View v) {
 4                 //启动另一个Activity,(通过action属性进行查找)
 5                 Intent intent = new Intent();
 6                 //设置动作(实际action属性就是一个字符串标记而已)
 7                 intent.setAction("com.example.smyh006intent01.MY_ACTION"); //方法:Intent android.content.Intent.setAction(String action)
 8                 intent.addCategory("com.example.smyh006intent01.MY_CATEGORY");
 9                 startActivity(intent);        
10             }
11         });
复制代码
复制代码

既然在Intent中增加了一个category,那么我们要在清单文件中去声明这个category,不然程序将无法运行。代码如下:

复制代码
复制代码
1             android:name=".SecondActivity">
2             <intent-filter>
3                  <action android:name="com.example.smyh006intent01.MY_ACTION"/>
4                  <category android:name="android.intent.category.DEFAULT" />
5                  <category android:name="com.example.smyh006intent01.MY_CATEGORY" />
6             </intent-filter>            
7         </activity>
复制代码
复制代码

此时,点击MainActicity中的按钮,就会跳到SecondActicity中去。

总结如下:

自定义类别: 在Intent添加类别可以添加多个类别,那就要求被匹配的组件必须同时满足这多个类别,才能匹配成功。操作Activity的时候,如果没有类别,须加上默认类别

 

4、data(数据):表示与动作要操纵的数据

  • Data属性是Android要访问的数据,和action和Category声明方式相同,也是在<intent-filter>中。
  • 多个组件匹配成功显示优先级高的; 相同显示列表。

Data是用一个uri对象来表示的,uri代表数据的地址,属于一种标识符。通常情况下,我们使用action+data属性的组合来描述一个意图:做什么

使用隐式Intent,我们不仅可以启动自己程序内的活动,还可以启动其他程序的活动,这使得Android多个应用程序之间的功能共享成为了可能。比如应用程序中需要展示一个网页,没有必要自己去实现一个浏览器(事实上也不太可能),而是只需要条用系统的浏览器来打开这个网页就行了。

【实例】打开指定网页:

MainActivity.java中,监听器部分的核心代码如下:

复制代码
复制代码
 1         button1.setOnClickListener(new OnClickListener() {            
 2             @Override
 3             public void onClick(View v) {
 4                 Intent intent = new Intent();
 5                 intent.setAction(Intent.ACTION_VIEW);
 6                 Uri data = Uri.parse("http://www.baidu.com");
 7                 intent.setData(data);                
 8                 startActivity(intent);        
 9             }
10         });
复制代码
复制代码

当然,上方代码也可以简写成:

复制代码
复制代码
1         button1.setOnClickListener(new OnClickListener() {            
2             @Override
3             public void onClick(View v) {
4                 Intent intent = new Intent(Intent.ACTION_VIEW);
5                 intent.setData(Uri.parse("http://www.baidu.com"));                
6                 startActivity(intent);        
7             }
8         });
复制代码
复制代码

第4行代码:指定了Intent的action是 Intent.ACTION_VIEW,表示查看的意思,这是一个Android系统内置的动作;

第5行代码:通过Uri.parse()方法,将一个网址字符串解析成一个Uri对象,再调用intent的setData()方法将这个Uri对象传递进去。

当点击按钮时,将跳到如下界面:

此时, 调用的是系统默认的浏览器,也就是说,只调用了这一个组件。现在如果有多个组件得到了匹配,应该是什么情况呢?

我们修改修改清单文件中对SecondAcivity的声明:

复制代码
复制代码
1         <activity 
2             android:name=".SecondActivity">
3             <intent-filter>
4                  <action android:name="android.intent.action.VIEW" />
5                  <category android:name="android.intent.category.DEFAULT" />
6                  <data android:scheme="http" android:host="www.baidu.com"/>                 
7             </intent-filter>            
8         </activity>
复制代码
复制代码

现在,SecondActivity也匹配成功了,我们运行程序,点击MainActicity的按钮时,弹出如下界面供我们选择:

我们可以总结如下:

  • 当Intent匹配成功的组件有多个时,显示优先级高的组件,如果优先级相同,显示列表让用户自己选择
  • 优先级从-1000至1000,并且其中一个必须为负的才有效

注:系统默认的浏览器并没有做出优先级声明,其优先级默认为正数。

优先级的配置如下:

在清单文件中修改对SecondAcivity的声明,即增加一行代码,通过来android:priority设置优先级,如下:

复制代码
复制代码
1         <activity 
2             android:name=".SecondActivity">
3             <intent-filter android:priority="-1">
4                  <action android:name="android.intent.action.VIEW" />
5                  <category android:name="android.intent.category.DEFAULT" />
6                  <data android:scheme="http" android:host="www.baidu.com"/>                                  
7             </intent-filter>            
8         </activity>
复制代码
复制代码

注:

Data属性的声明中要指定访问数据的Uri和MIME类型。可以在<data>元素中通过一些属性来设置:

android:scheme、android:path、android:port、android:mimeType、android:host等,通过这些属性来对应一个典型的Uri格式scheme://host:port/path。例如:http://www.google.com。

 

5、type(数据类型):对于data范例的描写

如果Intent对象中既包含Uri又包含Type,那么,在<intent-filter>中也必须二者都包含才能通过测试。

Type属性用于明确指定Data属性的数据类型或MIME类型,但是通常来说,当Intent不指定Data属性时,Type属性才会起作用,否则Android系统将会根据Data属性值来分析数据的类型,所以无需指定Type属性。

data和type属性一般只需要一个,通过setData方法会把type属性设置为null,相反设置setType方法会把data设置为null,如果想要两个属性同时设置,要使用Intent.setDataAndType()方法。

【任务】:data+type属性的使用 【实例】:播放指定路径的mp3文件。

具体如下:

新建工程文件smyh006_Intent02,MainActivity.java中按钮监听事件部分的代码如下:

复制代码
复制代码
 1         button.setOnClickListener(new OnClickListener(){
 2             @Override
 3             public void onClick(View v) {
 4                 Intent intent = new Intent();
 5                 intent.setAction(Intent.ACTION_VIEW);
 6                 Uri data = Uri.parse("file:///storage/sdcard0/平凡之路.mp3");
 7                 //设置data+type属性
 8                 intent.setDataAndType(data, "audio/mp3"); //方法:Intent android.content.Intent.setDataAndType(Uri data, String type)
 9                 startActivity(intent);                
10             }            
11         });
复制代码
复制代码

代码解释:

第6行:"file://"表示查找文件,后面再加上我的小米手机存储卡的路径:/storage/sdcard0,再加上具体歌曲的路径。

第8行:设置data+type属性  

运行后,当点击按钮时,效果如下:

上方界面中,使用的是小米系统默认的音乐播放器。

 

6、extras(扩展信息):扩展信息

是其它所有附加信息的集合。使用extras可以为组件提供扩展信息,比如,如果要执行“发送电子邮件”这个

动作,可以将电子邮件的标题、正文等保存在extras里,传给电子邮件发送组件。

 

7、Flags(标志位):期望这个意图的运行模式

一个程序启动后系统会为这个程序分配一个task供其使用,另外同一个task里面可以拥有不同应用程序的activity。那么,同一个程序能不能拥有多个task?这就涉及到加载activity的启动模式,这个需要单独讲一下。

注:android中一组逻辑上在一起的activity被叫做task,自己认为可以理解成一个activity堆栈。

 

三、Activity的启动模式:(面试注意)

Activity有四种启动模式:standard、singleTop、singleTask、singleInstance。可以在AndroidManifest.xml中activity标签的属性android:launchMode中设置该activity的加载模式。

  • standard模式:默认的模式,以这种模式加载时,每当启动一个新的活动,必定会构造一个新的Activity实例放到返回栈(目标task)的栈顶,不管这个Activity是否已经存在于返回栈中;
  • singleTop模式:如果一个以singleTop模式启动的activity的实例已经存在于返回桟的桟顶,那么再启动这个Activity时,不会创建新的实例,而是重用位于栈顶的那个实例,并且会调用该实例的onNewIntent()方法将Intent对象传递到这个实例中;

注:如果以singleTop模式启动的activity的一个实例已经存在于返回桟中,但是不在桟顶,那么它的行为和standard模式相同,也会创建多个实例;

  • singleTask模式:这种模式下,每次启动一个activity时,系统首先会在返回栈中检查是否存在该活动的实例,如果存在,则直接使用该实例,并把这个活动之上的所有活动统统清除;如果没有发现就会创建一个新的活动实例;
  • singleInstance模式:总是在新的任务中开启,并且这个新的任务中有且只有这一个实例,也就是说被该实例启动的其他activity会自动运行于另一个任务中。当再次启动该activity的实例时,会重新调用已存在的任务和实例。并且会调用这个实例的onNewIntent()方法,将Intent实例传递到该实例中。和singleTask相同,同一时刻在系统中只会存在一个这样的Activity实例。(singleInstance即单实例)

注:前面三种模式中,每个应用程序都有自己的返回栈,同一个活动在不同的返回栈中入栈时,必然是创建了新的实例。而使用singleInstance模式可以解决这个问题,在这种模式下会有一个单独的返回栈来管理这个活动,不管是哪一个应用程序来访问这个活动,都公用同一个返回栈,也就解决了共享活动实例的问题。(此时可以实现任务之间的切换,而不是单独某个栈中的实例切换)

 

其实我们不在清单文件中设置,只在代码中通过flag来设置也是可以的,如下:

1         Intent intent = new Intent(MainActivity.this,SecondActivity.class);
2         //相当于singleTask
3         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
4         startActivity(intent);

 

 

1         Intent intent = new Intent(MainActivity.this,SecondActivity.class);
2         //相当于singleTop
3         intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
4         startActivity(intent);

 

 

三、Intent的常见应用:

1、打开指定网页:(直接复制的上面的代码)

MainActivity.java中,监听器部分的核心代码如下:

复制代码
复制代码
 1         button1.setOnClickListener(new OnClickListener() {            
 2             @Override
 3             public void onClick(View v) {
 4                 Intent intent = new Intent();
 5                 intent.setAction(Intent.ACTION_VIEW);//方法:android.content.Intent.Intent(String action)
 6                 Uri data = Uri.parse("http://www.baidu.com");
 7                 intent.setData(data);                
 8                 startActivity(intent);        
 9             }
10         });
复制代码
复制代码

当然,上方代码也可以简写成:

复制代码
复制代码
1   button1.setOnClickListener(new OnClickListener() {            
2             @Override
3             public void onClick(View v) {
4                 Intent intent = new Intent(Intent.ACTION_VIEW);
5                 intent.setData(Uri.parse("http://www.baidu.com"));                
6                 startActivity(intent);        
7             }
8         });
复制代码
复制代码

第4行代码:指定了Intent的action是 Intent.ACTION_VIEW,表示查看的意思,这是一个Android系统内置的动作;

第5行代码:通过Uri.parse()方法,将一个网址字符串解析成一个Uri对象,再调用intent的setData()方法将这个Uri对象传递进去。

或者可以写成:

复制代码
复制代码
1         button1.setOnClickListener(new OnClickListener() {            
2             @Override
3             public void onClick(View v) {
4                 Uri uri = Uri.parse("http://www.baidu.com");
5                 Intent intent = new Intent(Intent.ACTION_VIEW,uri);//方法: android.content.Intent.Intent(String action, Uri uri)        
6                 startActivity(intent);        
7             }
8         });
复制代码
复制代码

 

2、打电话:

【方式一】打开拨打电话的界面:

 

1                 Intent intent = new Intent(Intent.ACTION_DIAL);
2                 intent.setData(Uri.parse("tel:10086"));
3                 startActivity(intent);  

运行程序后,点击按钮,显示如下界面:

 

【方式二】直接拨打电话:

 

1                 Intent intent = new Intent(Intent.ACTION_CALL);
2                 intent.setData(Uri.parse("tel:10086"));
3                 startActivity(intent);

要使用这个功能必须在配置文件中加入权限:(加一行代码)

 

1     <uses-sdk
2         android:minSdkVersion="8"
3         android:targetSdkVersion="16" />
4     <uses-permission android:name="android.permission.CALL_PHONE"/>

 

3、发送短信:

【方式一】打开发送短信的界面:action+type

 

1         Intent intent = new Intent(Intent.ACTION_VIEW);
2         intent.setType("vnd.android-dir/mms-sms");
3         intent.putExtra("sms_body", "具体短信内容"); //"sms_body"为固定内容
4         startActivity(intent); 

【方式二】打开发短信的界面(同时指定电话号码):action+data

 

1         Intent intent = new Intent(Intent.ACTION_SENDTO);
2         intent.setData(Uri.parse("smsto:18780260012"));
3         intent.putExtra("sms_body", "具体短信内容"); //"sms_body"为固定内容        
4         startActivity(intent);

4、播放指定路径音乐:action+data+type

 

1         Intent intent = new Intent(Intent.ACTION_VIEW);
2         Uri uri = Uri.parse("file:///storage/sdcard0/平凡之路.mp3"); ////路径也可以写成:"/storage/sdcard0/平凡之路.mp3"
3         intent.setDataAndType(uri, "audio/mp3"); //方法:Intent android.content.Intent.setDataAndType(Uri data, String type)
4         startActivity(intent);

5、卸载程序:action+data(例如点击按钮,卸载某个应用程序,根据包名来识别)

注:无论是安装还是卸载,应用程序是根据包名package来识别的。

 

1         Intent intent = new Intent(Intent.ACTION_DELETE);
2         Uri data = Uri.parse("package:com.example.smyh006intent01");
3         intent.setData(data);
4         startActivity(intent);

6、安装程序:action+data+type

 

1         Intent intent = new Intent(Intent.ACTION_VIEW);
2         Uri data = Uri.fromFile(new File("/storage/sdcard0/AndroidTest/smyh006_Intent01.apk"));    //路径不能写成:"file:///storage/sdcard0/···"
3         intent.setDataAndType(data, "application/vnd.android.package-archive");  //Type的字符串为固定内容
4         startActivity(intent);

注:第2行的路径不能写成:"file:///storage/sdcard0/···",不然报错如下:

疑问:通过下面的这种方式安装程序,运行时为什么会出错呢?

 

复制代码
复制代码
1     //通过指定的action来安装程序
2     public void installClickTwo(View view){
3         Intent intent = new Intent(Intent.ACTION_PACKAGE_ADDED);
4         Uri data = Uri.fromFile(new File("/storage/sdcard0/AndroidTest/smyh006_Intent01.apk"));    //路径不能写成:"file:///storage/sdcard0/···"
5         intent.setData(data);
6         startActivity(intent);
7     }
复制代码
复制代码

 

 

 

 

综上所述,完整版代码如下:

 

复制代码
复制代码
 1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 2     xmlns:tools="http://schemas.android.com/tools"
 3     android:layout_width="match_parent"
 4     android:layout_height="match_parent"
 5     android:paddingBottom="@dimen/activity_vertical_margin"
 6     android:paddingLeft="@dimen/activity_horizontal_margin"
 7     android:paddingRight="@dimen/activity_horizontal_margin"
 8     android:paddingTop="@dimen/activity_vertical_margin"
 9     android:orientation="vertical"
10     tools:context=".MainActivity" >
11     <Button 
12         android:id="@+id/button1_browsePage"
13         android:layout_width="match_parent"
14         android:layout_height="wrap_content"
15         android:onClick="browsePageClick"
16         android:text="打开指定网页"/>   
17     <Button 
18         android:id="@+id/button2_openDialPage"
19         android:layout_width="match_parent"
20         android:layout_height="wrap_content"
21         android:onClick="openDialPageClick"
22         android:text="打开拨号面板"/>
23     <Button 
24         android:id="@+id/button3_dialPhone"
25         android:layout_width="match_parent"
26         android:layout_height="wrap_content"
27         android:onClick="dialPhoneClick"
28         android:text="直接拨打指定号码"/>
29     <Button 
30         android:id="@+id/button4_openMsgPage"
31         android:layout_width="match_parent"
32         android:layout_height="wrap_content"
33         android:onClick="openMsgPageClick"
34         android:text="打开发短信的界面"/>
35     
36     
37      <Button 
38         android:id="@+id/button5_sendMsg"
39         android:layout_width="match_parent"
40         android:layout_height="wrap_content"
41         android:onClick="sendMsgClick"
42         android:text="给指定的人发短信"/>  
43         
44      <Button 
45         android:id="@+id/button6_playMusic"
46         android:layout_width="match_parent"
47         android:layout_height="wrap_content"
48         android:onClick="playMusicClick"
49         android:text="播放指定路径音乐"/>      
50      
51      <Button 
52         android:id="@+id/button7_uninstall"
53         android:layout_width="match_parent"
54         android:layout_height="wrap_content"
55         android:onClick="uninstallClick"
56         android:text="卸载程序"/>    
57      <Button 
58         android:id="@+id/button8_install"
59         android:layout_width="match_parent"
60         android:layout_height="wrap_content"
61         android:onClick="installClick"
62         android:text="安装程序"/>    
63         
64     
65 </LinearLayout>
复制代码
复制代码

MainActivity.java代码如下:

 

复制代码
复制代码
 1 package com.example.m06intent01;
 2 import java.io.File;
 3 import android.app.Activity;
 4 import android.content.Intent;
 5 import android.net.Uri;
 6 import android.os.Bundle;
 7 import android.view.Menu;
 8 import android.view.View;
 9 public class MainActivity extends Activity {
10     @Override
11     protected void onCreate(Bundle savedInstanceState) {
12         super.onCreate(savedInstanceState);
13         setContentView(R.layout.activity_main);
14     }
15     //打开指定网页
16     public void browsePageClick(View view){
17         Intent intent = new Intent(Intent.ACTION_VIEW);
18         intent.setData(Uri.parse("http://www.baidu.com/"));
19         startActivity(intent);  
20         
21     } 
22     
23     //打开拨号面板
24     public void openDialPageClick(View view){
25         Intent intent = new Intent(Intent.ACTION_DIAL);
26         intent.setData(Uri.parse("tel:10086"));
27         startActivity(intent);        
28     }
29     
30     //直接拨打指定号码
31     public void dialPhoneClick(View view){
32         Intent intent = new Intent(Intent.ACTION_CALL);
33         intent.setData(Uri.parse("tel:10086"));
34         startActivity(intent);        
35     }
36     
37     //打开发短信的界面:action+type
38     public void openMsgPageClick(View view){
39         Intent intent = new Intent(Intent.ACTION_VIEW);
40         intent.setType("vnd.android-dir/mms-sms");
41         intent.putExtra("sms_body", "具体短信内容"); //"sms_body"为固定内容
42         startActivity(intent);        
43     }   
44     
45     //打开发短信的界面(指定电话号码):action+data
46     public void sendMsgClick(View view){
47         Intent intent = new Intent(Intent.ACTION_SENDTO);
48         intent.setData(Uri.parse("smsto:18780260012"));
49         intent.putExtra("sms_body", "具体短信内容"); //"sms_body"为固定内容        
50         startActivity(intent);        
51     }      
52     
53     //播放指定路径音乐
54     public void playMusicClick(View view){
55         Intent intent = new Intent(Intent.ACTION_VIEW);
56         Uri uri = Uri.parse("file:///storage/sdcard0/平凡之路.mp3");  //路径也可以写成:"/storage/sdcard0/平凡之路.mp3"
57         intent.setDataAndType(uri, "audio/mp3"); //方法:Intent android.content.Intent.setDataAndType(Uri data, String type)
58         startActivity(intent);
59     } 
60     
61     //卸载某个应用程序,根据包名来识别
62     public void uninstallClick(View view){
63         Intent intent = new Intent(Intent.ACTION_DELETE);
64         Uri data = Uri.parse("package:com.example.smyh006intent01");
65         intent.setData(data);
66         startActivity(intent);
67     } 
68     
69     //安装某个应用程序,根据apk的文件名来识别
70     public void installClick(View view){
71         Intent intent = new Intent(Intent.ACTION_VIEW);
72         Uri data = Uri.fromFile(new File("/storage/sdcard0/AndroidTest/smyh006_Intent01.apk"));    //路径不能写成:"file:///storage/sdcard0/···"
73         intent.setDataAndType(data, "application/vnd.android.package-archive");  //Type的字符串为固定内容
74         startActivity(intent);
75     }
76     
77     
78     @Override
79     public boolean onCreateOptionsMenu(Menu menu) {
80         // Inflate the menu; this adds items to the action bar if it is present.
81         getMenuInflater().inflate(R.menu.main, menu);
82         return true;
83     }
84     
85 }
复制代码
复制代码

运行后,主界面如下:

作者:chaoyu168 发表于2016/8/15 13:54:00 原文链接
阅读:119 评论:0 查看评论

OPENGL---Ps 径向模糊算法(glsl)

$
0
0

本文转载自:  http://blog.csdn.net/zx6733090/article/details/40311689

功能本人之前也介绍过,但是这里转载,是因为这个版本是Opengl实现的,大家可以看一下,也可以顺便学习一下Opengl。

Opengl 实现径向模糊,可用于实现放射性效果:

#ifdef GL_ES
precision mediump float;
#endif


uniform vec2 centerpos;
uniform float GlowRange; 
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
void main()                      
{    
    vec4 clraverge=vec4(0,0,0,0);   
    float range=GlowRange,count=0,x1,y1;
 vec2 cpos=centerpos;    
    for( float j = 1; j<=range ; j += 1 )  
 {
    if(cpos.x-v_texCoord.x==0)
    {
       x1=v_texCoord.x;
    y1=v_texCoord.y+(cpos.y-v_texCoord.y)*j/(6*range);
    }
    else
   {
   float k=(cpos.y-v_texCoord.y)/(cpos.x-v_texCoord.x);
      x1=v_texCoord.x+(cpos.x-v_texCoord.x)*j/200;
         if((cpos.x-v_texCoord.x)*(cpos.x-x1)<0) x1=cpos.x;
   y1=cpos.y-cpos.x*k+k*x1;
   if(x1<0.0||y1<0.0||x1>1.0||y1>1) 
   {
     continue;
   }
   }
 clraverge+=texture2D( CC_Texture0, vec2(x1,y1) );
 count+=1;
 }
 clraverge/=count;
    gl_FragColor =clraverge;
}

centerpos为径向中心点,GlowRange为径向范围。其基本思想是模糊沿着中心点向外一条直线上的点,采样值可以自己确定,效果 好就ok了、、~

效果图:

作者:Trent1985 发表于2016/8/15 14:28:51 原文链接
阅读:106 评论:0 查看评论

React Native自定义导航栏

$
0
0

之前我们学习了可触摸组件和页面导航的使用的使用:
从零学React Native之09可触摸组件

从零学React Native之03页面导航

经过之前的学习, 我们可以完成一个自定义导航栏了, 效果如下:
这里写图片描述

我们需要创建一个 NaviBar.js 用来显示顶部的导航栏, 还需要四个界面(Page1.js,Page2.js,Page3.js,Page4.js)。 当然还需要修改index.android.js或者index.ios.js 用来处理4个界面的切换。

导航栏NaviBar 实现

这里, 我们就假设应用有4个栏目, 每个按钮的宽高比为 4:3
直接贴代码了:

import React, { Component } from 'react';
import {
    AppRegistry,
    StyleSheet,
    View,
    Text,
    TouchableHighlight,
} from 'react-native';

var Dimensions = require("Dimensions");
var totalWidth = Dimensions.get('window').width;// 屏幕宽度
let naviButtonWidth = totalWidth / 4;    //计算导航条每个宽度
let naviButtonHeight = naviButtonWidth * 0.75;   // 导航条每个高度
export  default class NaviBar extends Component {
    // 构造
    constructor(props) {
        super(props);
        this._tab0Pressed = this._tab0Pressed.bind(this);
        this._tab1Pressed = this._tab1Pressed.bind(this);
        this._tab2Pressed = this._tab2Pressed.bind(this);
        this._tab3Pressed = this._tab3Pressed.bind(this);
    }

    //四个按钮 被按下时处理函数
    _tab0Pressed() {
        this.props.onNaviBarPress(0);
    }

    _tab1Pressed() {
        this.props.onNaviBarPress(1);
    }

    _tab2Pressed() {
        this.props.onNaviBarPress(2);
    }

    _tab3Pressed() {
        this.props.onNaviBarPress(3);
    }

    render() {
        //通过属性得知哪个导航按钮是当前导航页, 这个导航用灰色背景
        //利用JavaScript数组的map函数,从一个数组对应生成另一个数组buttonColors
        // 看不懂该函数的,看下面的解释
        var buttonColors = this.props.naviBarStatus.map(function (aNumber) {
            if (aNumber == 0) return 'white';
            return 'gray';
        });
        return (
            //根View
            <View style={styles.naviRow}>
                <TouchableHighlight onPress={this._tab0Pressed}>
                    <View style={[styles.button,{backgroundColor:buttonColors[0]}]}>
                        <Text style={styles.textStyle1}>
                            条目一
                        </Text>
                    </View>
                </TouchableHighlight>
                <TouchableHighlight onPress={this._tab1Pressed}>
                    <View style={[styles.button,{backgroundColor:buttonColors[1]}]}>
                        <Text style={styles.textStyle1}>
                            条目二
                        </Text>
                    </View>
                </TouchableHighlight>
                <TouchableHighlight onPress={this._tab2Pressed}>
                    <View style={[styles.button,{backgroundColor:buttonColors[2]}]}>
                        <Text style={styles.textStyle1}>
                            条目三
                        </Text>
                    </View>
                </TouchableHighlight>
                <TouchableHighlight onPress={this._tab3Pressed}>
                    <View style={[styles.button,{backgroundColor:buttonColors[3]}]}>
                        <Text style={styles.textStyle1}>
                            条目四
                        </Text>
                    </View>
                </TouchableHighlight>
            </View>
        );
    }
}
// 声明属性, 方便使用当前组件
NaviBar.propTypes = {
    naviBarStatus: React.PropTypes.arrayOf(React.PropTypes.number).isRequired,
    onNaviBarPress: React.PropTypes.func.isRequired
};

//样式 
const styles = StyleSheet.create({
    naviRow: {
        flexDirection: 'row'
    },
    button: {
        width: naviButtonWidth,
        height: naviButtonHeight,
        justifyContent: 'center'
    },
    textStyle1: {
        fontSize: 20,
        textAlign: 'center'
    }
});

上面用到了, Map函数 ,
Map函数的作用是按照某种关系从原数组”映射”出新数组. 如下面求平方的例子:

var data= [1,2,3,4];
var newArray=data.map((item)=>{return item*item});
console.log(newArray);  //输出[1,4,9,16]

统一处理四个界面的切换

我们需要在index.android.js 或者index.ios.js 这个代码比较简单, 只需要导入四个界面, 用Navigator组件切换就可以了。

import React, { Component } from 'react';
import {
    AppRegistry,
    StyleSheet,
    Navigator
} from 'react-native';

import Page1 from './Page1';
import Page2 from './Page2';
import Page3 from './Page3';
import Page4 from './Page4';
class AwesomeProject extends Component {
    //告知Navigator 模块切换时的效果
    configureScene() {
        return Navigator.SceneConfigs.FadeAndroid;
    }
    //根据传递的信息, 处理界面的切换
    renderScene(router, navigator) {
        this._navigator = navigator;
        switch (router.name) {
            case 'Page1':
                return <Page1 navigator={navigator}/>;
            case 'Page2':
                return <Page2 navigator={navigator}/>;
            case 'Page3':
                return <Page3 navigator={navigator}/>;
            case 'Page4':
                return <Page4 navigator={navigator}/>;
        }
    }
    render() {
        return (
            //根View
            <Navigator
                initialRoute={{name:'Page1'}}
                configureScene={this.configureScene}
                renderScene={this.renderScene}/>
        );
    }
}
AppRegistry.registerComponent('AwesomeProject', () => AwesomeProject);

界面

上面的代码需要引入Page1 - Page4, 这个四个界面非常相似, 我们只贴其中一个了.
Page1.js

import React, { Component } from 'react';
import {
    View,
    StyleSheet,
} from 'react-native';
import NaviBar from './NaviBar';

export  default class Page1 extends Component {
    // 构造
    constructor(props) {
        super(props);
        // 初始状态
        this.onNaviBarPress = this.onNaviBarPress.bind(this);
    }

    render() {
        // 不同的Page,需要修改下面的这个数组, 通过数组控制导航栏条目显示状态
        var naviStatus = [1, 0, 0, 0];
        return (
            <View style={styles.container}>
                <NaviBar naviBarStatus={naviStatus}
                         onNaviBarPress={this.onNaviBarPress}/>
                <View style={styles.whatLeft}/>
            </View>
        );
    }
    //不同的page需要修改返回值
    onNaviBarPress(aNumber) {
        switch (aNumber) {
            case 0:
                return;
            case 1:
                //通过replace切换
                this.props.navigator.replace({
                    name: 'Page2'
                });
                return;
            case 2:
                this.props.navigator.replace({
                    name: 'Page3'
                });
                return;
            case 3:
                this.props.navigator.replace({
                    name: 'Page4'
                });
                return;
        }
    }
}
const styles = StyleSheet.create({
    container: {
        flex: 1
    },
    whatLeft: {  // 组件定义了一个上边框
        flex: 1,
        borderTopWidth: 1,
        borderColor: 'black',
        backgroundColor:'red' //每个界面背景颜色不一样
    }
});

顺便指出两点: 当根View没有指定背景色时, 默认值是一种灰色; 当子View没有指定背景色时,会继承父View的背景色。

更多精彩请关注微信公众账号likeDev,公众账号名称:爱上Android。
这里写图片描述

作者:yulianlin 发表于2016/8/15 17:32:43 原文链接
阅读:54 评论:0 查看评论

自定义遮盖层,帮你完成简单的操作引导

$
0
0

转载请注明出处:王亟亟的大牛之路

最近忙的起飞,本来想周末写的东西结果拖到了今天,不过没事。下午抽出点时间把工作做了下,开源给大家。

话不多说,先安利:https://github.com/ddwhan0123/Useful-Open-Source-Android(各个模块已经基本拆出来了,还剩下动画/自定义控件/疑难杂症/资料部分)


先看下实现的效果

这里写图片描述

这里写图片描述

来看下项目目录(很少就一个实现类,一个工具类)

这里写图片描述

简单讲一下这里有什么以及怎么用:

这是一个继承于RelativeLayout的自定义View

public class GuiderLayout extends RelativeLayout

可以做到 2种样式,圆 and 方

分别是

public static final int CLTP_RECT = 1;

public static final int CLTP_CIRCLE = 2;

有3中位置关系,分别是 below above right-top(也就是图中箭头以及文字于圈圈的关系)

那如何去show这个试图呢?

 guiderLayout.showGuider(button1, "common", GuiderLayout.CLTP_CIRCLE);


 public void showGuider(View view, final String tag, int clipMode)
传入3个参数:
1,被“圈”的View
2,"身份证"tag以及位置关系(为了易用没做自定义attrs.xml的行为)
3,“圈类型”,上面有提到,圆/方 两种样式

那怎么去掉这一堆“引导内容呢”?(就是箭头啊,文字啊这些)

guiderLayout.showNoGuide();
这边有一点没做好,没封装彻底,还要多写一行来隐藏“灰色的底板”,之后的会做2期提升,完善这部分
guiderLayout.setVisibility(View.GONE);

使用并不复杂,主要是说下tag这部分
需要show的layout (箭头,文字那个)要设置一个tag,像这样

   android:tag="common,below"

传入2个参数,用 “,”分割

第一个参数是“身份证”(自己写,唯一就行)

第二个参数是 “位置关系”(上面提到的那3个)

具体怎么用可以看源码,地址如下

项目地址:https://github.com/ddwhan0123/GuiderLayout

上一个高斯模糊的Dialog做了一些更新,有兴趣的也可以看下,地址如下:https://github.com/ddwhan0123/BlurPopupWindow

再贴下类似的遮盖层实现的开源库:

https://github.com/iammert/MaterialIntroView

https://github.com/hongyangAndroid/Highlight

作者:ddwhan0123 发表于2016/8/15 17:48:31 原文链接
阅读:278 评论:5 查看评论

设计模式六大原则: 辅导班的因材施教 -- 接口隔离原则

$
0
0

我的女朋友小肉是一名光荣的辅导班老师,说来惭愧,我上初中那会儿最讨厌辅导班老师了,每天上学都这么累了,晚上还得去见辅导班老师,神烦,奈何目前的教育机制下,很多家长认为辅导班是提高成绩比较靠谱的方式,导致这个行业市场很大。

小肉教三个水平不同的小班,那天看她在准备讲义和试题,同一章内容需要做三份,其中很多内容都是重复的,自诩设计模式略懂一二的我跟她说:

你这个讲义跟我敲代码很像,相似的内容这么多,直接复制粘贴容易出问题啊,还不如把公共的部分提一个接口,然后让三种水平的讲义都实现这个接口

比如这样:

教案接口,指定所有班级要做的事:

/**
 * 教案接口,指定共同内容
 */
public interface TeachingPlanImpl {
    /**
     * 教授基础知识
     */
    void teachBaseKnowledge();

    /**
     * 教授拓展知识
     */
    void teachExtraKnowledge();

    /**
     * 教授拔高知识
     */
    void teachComplexKnowledge();

    /**
     * 布置简单作业
     */
    void assignBaseHomeWork();

    /**
     * 布置提升作业
     */
    void assignExtraHomeWork();

    /**
     * 布置复杂作业
     */
    void assignComplexHomeWork();
}

然后让三个班级的讲义实现这个接口:

/**
 * 基础班教案
 * Created by zhangshixin on 8/15/2016.
 */
public class TeachPlanBase implements TeachingPlanImpl {
    @Override
    public void teachBaseKnowledge() {

    }

    @Override
    public void teachExtraKnowledge() {

    }

    @Override
    public void teachComplexKnowledge() {

    }

    @Override
    public void assignBaseHomeWork() {

    }

    @Override
    public void assignExtraHomeWork() {

    }

    @Override
    public void assignComplexHomeWork() {

    }
}

//其余两个拔高班 TeachPlanExtra 、奥数班 TeachPlanComplex 教案类似,不再列举

这样你有需要改动的时候修改一处就好了,多省劲啊哈哈(得意脸)

没想到肉肉说:

可是普通班的孩子只要掌握基础就好了啊,他们要是发现自己不会的那么多该多难受 T.T 。基础班的只要基础知识和题,拓展班也不需要做奥数啊。而且哦,万一我的模板教案打错了,一下次还影响了三个班级的孩子。

没有好处反而有可能带来坏处,我觉得你设计的不好!

.

你!! 好吧,我竟无言以对

抽象、封装,不是要把公共的都提出去吗?带着疑问我开始求医问药,直到发现了:

接口隔离原则 ISP (Interface Segregation Principle)

定义:

客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
接口应该是内聚的,应该避免“胖”接口。一个类对另外一个类的依赖应该建立在最小的接口上,不要强迫依赖不用的方法,这是一种接口污染。

接口存在就是为了把细节和抽象分离开来,但是如果一个接口抽象的内容太多,其实就等于没有抽象。一个过于臃肿的接口会给它的实现类带来很多压力,比如上述例子讲的教案问题,对胖接口的修改会影响到很多实现类,有时候可能会带来大麻烦。

正确的方法是将胖接口分割成几个小接口,因材施教,不要给客户端暴露不需要的接口。

这里比较纠结的是接口究竟怎么算大,怎么算小,接口隔离原则也没有告诉我们,我们需要注意的就是接口的实现类尽量少实现不需要的方法,至于那个度还需要自己把握。

总结:一个诸葛亮不如 N 个裨将,万一诸葛亮病了呢

还容易混淆的是 单一职责原则和接口隔离原则的区别?

  • 接口隔离原则强调的是设计时的架构分离,把不同功能分给不同的接口,让实现类避免少了解与己无关的方法、通过实现不同接口保证与外部的耦合;
  • 单一职责原则强调的是 实现时的职责分离,具体功能下的不同实现要封装在不同的模块,尽量避免牵一发而动全身。

感谢:

http://my.oschina.net/heweipo/blog/422796
http://blog.csdn.net/zhengzhb/article/details/7296921

作者:u011240877 发表于2016/8/15 18:33:14 原文链接
阅读:4 评论:0 查看评论

Android 必知必会-Android Splash 页秒开之细节处理

$
0
0

如果移动端访问不佳,请访问 –> Github版

背景

今天阅读了两篇 Android Splash 页秒开的文章,就上手试了试,效果确实不错,不过在使用过程中发现个小的问题,应用是发现在 Android 6.0 系统下 APP 启动的时候有个默认的动画,如果按照文章介绍的直接启动下一个加载数据的页面会出现页面闪动,有点美中不足的感觉。

解决方法

解决方法很简单,我这里做出了两处修改:延迟启动添加淡出的过场动画

SplashActivity

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                Intent starter = new Intent(SplashActivity.this, xxxActivity.class);
                startActivity(starter);
                finish();
                overridePendingTransition(R.anim.stand,R.anim.splash);
            }
        },500);

    }

stand.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/accelerate_interpolator">
    <translate
        android:duration="200"
        android:fromXDelta="0%p"
        android:toXDelta="0%p"
        />
</set>

splash.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha
        android:fromAlpha="1.0"
        android:toAlpha="0.0"
        android:duration="200"
        />
</set>

总结

首先贴一下阅读到的两篇文章:

先感谢下博主的认真总结,才能让我们把自己的 APP 启动体验做的这么完美。

在实际开发中,我们可能会遇到很多复杂的情况,一篇博客或者教程也好,只能记录最核心的一些东西,学习的时候还是需要多多思考的。

PS:你可以通过下面的方式和我联系

作者:ys743276112 发表于2016/8/16 17:28:49 原文链接
阅读:54 评论:0 查看评论

iOS开发------Apple Pay(证书配置篇)

$
0
0

苹果支付(Apple Pay)中国上线有段时间了,也许是因为国人还不太习惯这种支付方式,也有可能楼主待的城市比较小,从日常生活中感觉用它做支付方式的人还不是很多。但楼主一般能用Apple Pay的时候基本也不太用其他的支付方式(除非有优惠,哈哈)

在Apple Pay上线之后,美团、京东等常用支付的App都在第一时间支持了它,上线第二天楼主也绑卡体验了一下传说中的Apple Pay,感觉好用的同时还真的挺高大上的呢,毕竟直接从App端弹出支付界面,也不需要跳转第三方支付App了。随着上线时间的延长,各大银行的ATM机都已经支持ApplePay了(不得不说,提款真的很方便呢)。

趁这两天稍微有点闲时间,查看公司项目的时候,发现由于目前公司的项目支付模块还没有支持Apple Pay(至于以后会不会有,还是要看产品的 0.0 ),于是就开始关注了Apple Pay,是为以后支持做准备也好,楼主个人兴趣也好,了解一下Apple Pay这个框架PassKit.framework也是极好的。这里就记录以下楼主研究的过程,中间遇到的问题也会在相应的位置用该文字的颜色标注,希望能够帮助遇到同样问题的小伙伴们吧。

先来一张高大上的支付界面吧,如果是真机,下面应该会出现一个Touch验证,当然个别银行在验证完毕之后还回跳转到输入PIN码的界面。PS:不是楼主不真机截图,真的办不到啊,一截图Pay VC就消失了


下面的顺序就是我创建项目的顺序,当然,比如如何创建一个App ID这种事,能google到很多写的很好的文章,所以就不在博文中缀余了。

由于篇幅太长反而会不太好,所以本篇就只记录了相关证书配置的过程,如果想瞅瞅如何使用PassKit来调用Apple Pay的原生接口敬请期待呀 (好无力的保证)

创建一个App ID

  • 如果出现下面问话,楼主觉得好尴尬呀..
  • 问: 我没有开发者账号怎么办?
  • 楼主: - - 这个不太好办了吧,毕竟后面还需要配置支付id以及加密证书(没有此证书在真机上跑会出现问题,此问题在下面已有提及)呢…如果有什么好办法,也请告知一下呀..
  • 楼主(悄悄): 你可以使用公司的开发者账号,毕竟我们又不影响上线,只创建一个App ID应该是没有问题的吧?

也算是为了保密一下,这里只能看到出Demo的App ID,暂且就叫它ApplePayPractise了.(是不是暴露了楼主其实用的账号也是公司账号的事实啊,哈哈哈)



创建Merchant ID

简单来讲,我们在开商店的时候是不是需要一个营业执照才能成为合法的商人呢,它的作用就好像是我们App的营业执照,它就是App合法使用Apple Pay的身份证明,创建步骤如下:

创建+

ApplePayDemoYue是我做练习的时候注册的一个ID,不要介意,还是可以继续添加的嘛:


填写相关消息

按照图片上的注解填好信息,点击Continue,随后Register即可


配置支付域

点击创建好的Merchant ID,点击Edit,在没有配置加密证书之前,会有一个创建加密证书的选项,点击进入一个服务配置:(通过英文我们可以看出,每一个”营业执照”必须有一个专属的加密证书,而且是必须的)

下面的意思就是:这个商人标志是否仅仅在中国才被关联(也就是说你这个营业执照在别的区域要不要使用),默认是No,这里调成Yes吧,通常来讲,其他国家的支付最好的方法就是在创建一个Merchant ID,毕竟在中国百分之好多好多都是人民币支付的吧?之后它就会跳转到下面配置App Pay RSA的步骤中了..

这样我们的“营业执照”算是办理完毕了.

注册App Pay RSA证书

既然营业执照办理完毕了,是不是我们就可以赚钱了,但是为了我们money的安全性,是不是还需要对相关信息进行加密处理呢,这个证书就是负责为我们加密的证书,既然选择用RSA加密,就是说明这段信息是需要在某个位置解密的,当然,这不是我们App端思考的问题了。

无法使用Apple Pay,检查此应用的设置并确定其设计可使用Apple Pay

如果不配置这个证书,其实在模拟器上是可以运行的,毕竟模拟器会帮我们模拟一些东西,之前的预览图就是在模拟器上运行的;但是在真机上就会报如下错误:”XXX”中无法使用Apple Pay,检查此应用的设置并确定其设计可使用Apple Pay,具体如下图:


配置

下面是常见的配置证书界面,通过点击左侧的Certificates->All就可以看到该界面了,点击右上角的加号进行证书的添加。




选择Production(这不是发布的证书么,用同一个有什么关系么?)下的Apple Pay证书即可,然后一直continue,该填的就填,一路过关斩将。



配置完成后,下载到电脑上,双击安装到钥匙串即可,下图是楼主的钥匙串:



使用Apple Pay的证书基本步骤都已经记录完毕,是不是感觉好麻烦,和money打交道嘛,严谨点毕竟是好的,如果上文中有什么不对的地方,还请告知一下,3Q。

如何使用想留在下一篇,感觉这篇幅点长了,敬请期待0.0 Thanks

作者:RunIntoLove 发表于2016/8/16 17:30:32 原文链接
阅读:13 评论:0 查看评论

深入学习中央调度(GCD)--第一部分

$
0
0
     更新说明:查阅我们基于iOS8.0和Swift下中央调度(https://www.raywenderlich.com/79149/grand-central-dispatch-tutorial-swift-part-1)教程这块的更新版本。
尽管中央调用(简称GCD)已经存在一段时间了,但并不是每个人都知道如何有效地使用它。这是可以理解的,并发本身就是棘手的,然而基于C语言的GCD API看起来像一套深入OC世界的弯角(转换器)。这个系列教程分两部分,深入地介绍中央调度(GCD)。 在这两部分中,第一部分解释了什么是GCD以及GCD常用的几个基本函数,在第二部分中,将会介绍几个GCD提供的更高级的功能。 **

什么是GCD?

** 
libdispatch俗称GCD,苹果提供的库,用以支持在iOS和OS X的多核硬件上执行并行代码。它有以下几个有点: 
1、GCD可以通过延缓耗时的计算任务放在后台运行来提高App的响应能力 
2、GCD提供了比加锁和线程更加简单的并发模型来避免并发bugs 
3、GCD可以使用高性能的执行单元优化代码,比如常用的模式:单例 
本教程假设你已经对blocks和GCD有一个基本的了解,如果是全新接触GCD,可以查阅供初学者;了解学习要点的基于iOS的多线程处理和中央调度。

 **

GCD术语

** 
要理解GCD,需要能应付自如几个跟线程和并发相关的概念。这些可能既模糊又微妙,所以在GCD的上下文中花点时间去简要回顾一下它们。 
1、串行与并行 
这俩术语描述了任务被执行时彼此的关系。串行执行任务每次执行一个任务,并发执行任务可能在同一时间执行多个任务。 
尽管这些术语有广泛的应用,但对于该教程来说,你可以把一个任务当做是一个OC代码块。不知道什么是块(block)?请查阅在iOS5下如何使用blocks。实际上,你也可以以函数指针的方式使用GCD,但在大多数情况下这样使用起来更加棘手。Blocks是更加简单的。 
2、同步与异步 
在GCD下,这俩术语描述了当一个功能完成之后与之关联的另一个任务功能如何请求GCD调用执行。同步意味着仅当任务按序执行完毕之后才会返回。 
异步,换句话说,就是立即返回预定的任务要执行但是不会等待。因此,异步不会阻塞当前线程的执行继续向下执行。 
注意,当你看到一个阻塞当前线程、函数或操作的的同步操作时,不要弄混了。这个动作块描述了一个功能是如何影响它的线程,并且没有连接到名词块(描述了一个在OC中的字面匿名函数且定义了一个提交到GCD的任务)。 
3、临界区 
这是一段不能被并发执行的代码,那就是,同时只能有一个线程执行。这就是一般并行进程访问共享资源(比如变量)的代码变坏的原因。 
4、争用条件 
这种情况是由软件系统依赖一个特殊的序列或在一个不受控制的事件(如:程序的并发任务的确切执行顺序)执行时间下产生的。争用情况可能产生不可预期的行为,而且不是立即可以通过代码检查就能发现的。 
5、死锁 
在大多数情况下,两个(有时更多)元素被说成是线程死锁是因为他们陷入了彼此等待而不能正常的完成或执行其他行为。一个不能结束是因为正在等待另一个结束。另一个不能完成是以为在等待第一个结束。 
6、线程安全 
线程安全的代码可以安全地被多个线程或并发任务调用而不会引起任何问题(如:数据异常、崩溃等)。线程不安全的代码同一时间下仅仅可以在一个上下文中运行。一个线程安全的例子就是不可变字典,你可以在多个线程中同时使用而不会出问题。换句话说可变字典不是线程安全的,因为同一时间下,仅可以可以在一个线程中访问(安全而不出问题)。 
7、上下文切换 
上下文切换是指当一个程序存储和恢复执行状态(当你在单个进程中在不同线程间切换时)。这中程序在你写多任务程序时很常见,但是也带来了一些额外的开销作为代价。 
并发以并行 
并发和并行常常被同时提到,因此简要的说明下两者之间的区别还是值得的。 
分离的并发代码可以被“同时”执行。然而,这是由系统决定如何发生-或者如果完全发生的话。多核心得设备同时执行多个线程通过并行。然而在单核心设备中为了达到并发,运行一个线程时必须通过上下文切花来运行另一个线程。这通常发生的足够快,我们可以假想它按下图方式执行:

这里写图片描述 
尽管你可能会在GCD下写代码以使用并发执行,但最终是由GCD决定多少并行是必须的。并行必定并发,但是并发不能保证并行。 
这里更深层点的问题是,并发实际上是结构上的。当你在头脑中构思GCD代码时,就要规划代码结构以拆分为可同时运行的工作片和可以不必同时运行的任务。如果想更深入地研究这个问题,查阅这个精彩的演讲(this excellent talk by Rob Pike.)。 
队列 
GCD提供了调度队列以处理代码块,这些队列管理你提交到GCD的任务并按FIFO顺序执行。这保证了第一个进入队列的任务是第一个开始执行的,第二个添加到队列的将第二被执行,接下来按序。 
所有的调度队列对他们自己而言是线程安全的,可以在不同的线程中同时访问。GCD的优势是明显的(当你理解自己不同部分的代码是如何线程安全地访问调度队列时)。关键就是选择合适的调度队列类型和合适的dispatching函数提交自己的任务到队列。 
这节中,将看到两种类型的调度队列,GCD提供的特定队列以及通过一些列子说明如何使用GCD调度函数添加任务到调度队列。 
1、串行队列 
在串行队列中的任务一次执行一个,每个任务的开始必须是前面的任务完成之后。当然,也无需知道一个代码段何时结束及下一个何时开始,如图所示: 
这里写图片描述 
这些任务的执行时间是由GCD控制的,能知道的就是一次执行一个任务,按照添加到队列的顺序按序执行。 
由于在串行队列中不会有两个任务并发执行,也就没有同时访问临界区的并发问题,以保护临界区不会被争竞条件影响。因此,访问临界区的唯一方式就是通过提交到调度队列的任务访问,保证临界区安全。 
2、并发队列 
在并发队列中的任务仅仅能保证按照添加进的顺序启动,and这也是能保证的所有。元素可能一任何顺序结束,你也不能确定下一个block还要多长时间才能开始,同时在执行的blocks数目也不能确定,这都是GCD决定的。 
下面的图表展示了一个任务执行的示例,其中GCD控制了4个并发任务: 
这里写图片描述 
备注:现在block1,2和3运行很快,一个接一个。block1开始执行花费了一点时间在block差不多执行结束后才开始。同样的,在block2开始后block3也开始执行了但并不是block2结束后才开始。 
何时开始一个block执行完全由GCD决定。如果执行一个block的时间超时了,GCD会决定是否在另一个可用的核心上开始另一个任务或者切换上下文去执行另一个不同的 代码块。 
令人欣喜的是,GCD提供了至少5个特别的队列类型可供选择。 
3、队列类型 
首先,系统提供了一个特别的串行队列成为主队列。像任何串行队列一样,在这个队列中一次只能执行一个任务。然而,它可以保证所有的任务都在主线程(必须要保证所有更新UI的操作必须在这个线程执行)中执行。这个队列是一个用于接收UIView消息和通知的队列。 
该系统还提供了其他几个并发队列。这些统称为全局调度队列。有4个不同优先级的全局队列:background, low, default, high.值得一提的是,苹果的api也使用这些队列,因此你添加任何任务到这些队列,其中任务不只有你添加的。 
此外,你也可以创建你自定义的串行或并行队列。这意味着至少有5个队列任由你处置:主队列,4个全局队列,再加上任何一个你添加的自定义的队列。 
这就是调度队列的“伟大蓝图”。 
GCD的艺术来源于选择合适的队列去提交任务。最好的经验就是通过下面的例子学习,在哪里我们根据长期经验提供了一些一般性的建议。

开始

 由于本教程的目的是既要简单又要安全的从不同的线程调用代码,你将从头到尾完成这个GoodPuff项目。
      GoodPuff是一个非优化的,非线程安全的app。在这里你要瞪大眼睛去分辨COre image API的使用。对于基本的图片来说,你可以从相册库中选择也可以从一系列未知的图片url下载使用 。
      Download the project here.
      一旦下载好,提取到一个合适的位置,用Xcode打开并运行它,看起来会像下面一样:

这里写图片描述
注意:当你选择下载图片选项时,UIA了人VIew会过早的弹出,浙江在本系列的第二部分修复。 
在这个项目中使用了四个类: 
PhotoCollectionViewController:这是启动app后的第一个视图控制器。它以缩略图的形式展示所有选中的图片。 
PhotoDetailViewController:这个以大图的形式在UIScrollView中展示图片 
Photo:这是一个类聚合,其可以从NSURL或ALAsset创建图片。该类提供图片,缩略图或一个下载图片的状态。 
PhotoManager:该类管理所欲照片实例。

用dispatch_sync处理后台任务

回过头来看该app,从相册库添加一些图片或使用网络下载一些。 
关注下在点击UICollectionViewCell后,花费了多长时间去实例化显示一个新的PhotoDetailViewController,有明显的滞后,尤其是在反应慢的设备上预览大图时。 
在很多复杂的环境下,执行UIViewController’s viewDidLoad很容易过载,在新视图显示之前常常要等待较长时间,在加载时不是必要的工作可以放在后台处理。 
这听起来像是异步工作。 
打开PhotoDetailViewController,替换viewDidLoad用下面的实现:

- (void)viewDidLoad
{ 
    [super viewDidLoad];
    NSAssert(_image, @"Image not set; required to use view controller");
    self.photoImageView.image = _image;

    //Resize if neccessary to ensure it's not pixelated
    if (_image.size.height <= self.photoImageView.bounds.size.height &&
        _image.size.width <= self.photoImageView.bounds.size.width) {
        [self.photoImageView setContentMode:UIViewContentModeCenter];
    }

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // 1
        UIImage *overlayImage = [self faceOverlayImageFromImage:_image];
        dispatch_async(dispatch_get_main_queue(), ^{ // 2
            [self fadeInNewImage:overlayImage]; // 3
        });
    });
}

上面是将要修改的代码。 
1、首先从把任务从主线程放到全局队列中。因为这是dispatch_async(异步),代码块被异步提交意味着将在从线程中调用。这可以让viewDidLoad可以尽快在主线程中执行完,让加载感觉特别快。同时,图片加载开始执行,将在之后某个时间完成。 
2、这时候,图片加载处理已完成,你已经生成一个新的图片。你可以拿新图片去更新显示。到主队列中添加一个新的工作。记住,只能在主线程中更新UI。 
3、最后,在fadeInNewImage中更新UI。 
生成并运行app,选择图片你会发现试图控制加载明显更快,并在短暂的时间后显示大图。这提供了一个不错的查看大图的效果。 
同样的,如果你试着加载一个出奇巨大的图片时,这个app也不会再加载视图控制器的时候卡住,同时app可以很好的扩展。 
正如上文提到的,dispatch_async将添加block到一个队列并立即返回。该任务将在一段时间之后被GCD决定执行。当需要执行一个网络操作或cpu耗时的任务时放在后台不会阻塞当前线程的执行。 
下面是一个如何、何时使用dispatch_async的各种队列类型的快速向导: 
自定义串行队列:当要后台串行执行任务、要跟踪它时,这是一个不错的选择。这消除了资源争用,因为你已经知道同一时间只能有一个任务执行。注意,如果你需要从一个方法获取数据,必须内嵌另一个 block进去同时考虑采用dispatch_sync方式。 
主队列(串行):在一个并行的队列中完成任务后去更新UI,选择它。这样做的话,将内嵌另一个block到block中。同理,如果在主队列中调用dispatch_async,只能保证新任务在当前方法结束后一段时间内将会执行。 
并行队列:要在后台执行非UI工作可以选择它。

延时工作dispatch_after 
考虑一下app的用户体验,当用户第一次打开app时可能会很困惑,不知道要做什么。 
展示一个提示信息可能是一个不错的主意,当在PhotoManager中没有任何照片时。但是,你也需要考虑用户的眼睛是如何浏览屏幕主页的,如果你展示图示信息太快(一闪而过)的话,他们可能根本没有看清视图中显示的内容。在显示提示信息时加上1~2秒的延时足够吸引用户注意了。 
添加下面的代码再执行去试着实现显示延时:(showOrHideNavPromote PhotoCollectionViewController)

- (void)showOrHideNavPrompt
{
    NSUInteger count = [[PhotoManager sharedManager] photos].count;
    double delayInSeconds = 1.0;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); // 1
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ // 2
        if (!count) {
            [self.navigationItem setPrompt:@"Add photos with faces to Googlyify them!"];
        } else {
            [self.navigationItem setPrompt:nil];
        }
    });
}

生成并运行app。轻微的延迟,将吸引用户的注意,提示他们该怎么做。

dispathc_after工作就像一个延时的dispatch_async。你依然没有实际执行时间的控制权,但是可以在其返回之前取消。 
想知道什么时候使用dispatch_after吗? 
1、自定义串行的队列:在自定义串行队列上小心使用,最好在注队列使用。 
2、主队列:主队列可以使用dispatch_after,Xcode有一个nice模板去自动创建使用它。 
3、并发队列:在自定义的并发队列上使用dispatch_after时要小心,而且很少用。坚持在主队列使用它。

使单例模式线程安全

单例模式:既爱又恨,在iOS和在服务器器系统上的web一样受欢迎。 
复杂的单例关系常常不是线程安全的。这种关系要合理的使用:单例模式就是常常多个视图控制器同时访问单个单一实例。 
对单例来说,线程关系涉及到初始化、读取和写入信息。 
PhotoManager类就是单例类,在当前状态下就面临这些问题。为了更快的看到问题所在,将要在单例中创建一个受控的争用条件。 
定位到PhotoManager.m 然后找到sharedManger,代码想下面这样:

+ (instancetype)sharedManager
{
    static PhotoManager *sharedPhotoManager = nil;
    if (!sharedPhotoManager) {
        sharedPhotoManager = [[PhotoManager alloc] init];
        sharedPhotoManager->_photosArray = [NSMutableArray array];
    }
    return sharedPhotoManager;
}
 当前状态下代码是很简单的,你创建了一个单例然后初始化了一个私有数组(photoArray)。
但是,if条件分支不是线程安全的,如果你多次调用它,很有可能出现在线程A中进入if代码段然后在执行sharedManager分配之前进行了上下文切换。然后在线程B中可能也进入if条件,分配了一个单实例而后退出。当系统上下文切换回线程A后,继续分配领一个单实例后退出。同时将产生两个单实例,这不是我们想看到的。
 为了防止这种情况发生,替换sharedmanager方法用下面的实现:
+ (instancetype)sharedManager
{
    static PhotoManager *sharedPhotoManager = nil;
    if (!sharedPhotoManager) {
        [NSThread sleepForTimeInterval:2];
        sharedPhotoManager = [[PhotoManager alloc] init];
        NSLog(@"Singleton has memory address at: %@", sharedPhotoManager);
        [NSThread sleepForTimeInterval:2];
        sharedPhotoManager->_photosArray = [NSMutableArray array];
    }
    return sharedPhotoManager;
}
 上面代码中,在线程休眠方法中强制进行上下文切换。打开AppDelegate.m添加如下代码:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    [PhotoManager sharedManager];
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    [PhotoManager sharedManager];
});
 这将创建多个异步并发调用来实例化单例并会出现上文所述的争用情况。
 生成并运行项目,检查控制台的输出,将会看到多个单实例初始化,如下所示:

这里写图片描述
注意:有几行显示了单例实例不同的地址,偏离了单例的初衷,不是吗? 
输出显示只应被执行一次的临界区却被执行了多次。诚然,现在是你强制这种情况发生,但是你可以想象一下这种情况也会在不经意间偶然出现。 
注:基于系统之上的其他事件很难控制,一系列的NSLog打印证明这点。线程问题很难跟踪因为它很难复现。 
为了纠正这种问题,当运行在临界区的if条件中时,初始化代码应该仅被执行一次并阻塞其他实例。这个正是dispatch_once做的事情。 
替换单例中中的if条件语句用下面的单例初始化实现:

+ (instancetype)sharedManager
{
    static PhotoManager *sharedPhotoManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [NSThread sleepForTimeInterval:2];
        sharedPhotoManager = [[PhotoManager alloc] init];
        NSLog(@"Singleton has memory address at: %@", sharedPhotoManager);
        [NSThread sleepForTimeInterval:2];
        sharedPhotoManager->_photosArray = [NSMutableArray array];
    });
    return sharedPhotoManager;
}

生成并运行app,查阅控制台输出,你讲看到仅有一个单实例被初始化,这才是我们想要的单例模式。
现在既然理解了防止争用条件的重要性,就删除AppDelegate.m中添加的diapatch_async语句然后替换单里初始化的实现用下面的实现:

+ (instancetype)sharedManager
{
    static PhotoManager *sharedPhotoManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedPhotoManager = [[PhotoManager alloc] init];
        sharedPhotoManager->_photosArray = [NSMutableArray array];
    });
    return sharedPhotoManager;
}
 dispatch_once 执行块一次,且以线程安全的方式仅仅执行以一次。不同的线程试图访问临界区,代码执行到dispatch_once,当一个线程已经在代码块中是将独占临界区知道完成。

这里写图片描述 
应该指出的是这仅仅是共享实例的线程安全,并不一定类线程安全。你也可以有其他的临界区,例如:然和可操作的内部数据。这些需要使用线程的安全的其他方式,譬如:同步党文数据,下面将会看到。

处理读和写的问题

线程安全的实例化单例不是唯一的问题。如果单例的属性是一个可变的对象,你就需要考虑对象本身是否是线程安全的。 
Foundation中的基础容器是线程安全的吗?答案是-不是的。苹果维护了一系列有益的非线程安全的基础数据类型。 
尽管,很多线程可以读取一个可变的数组而没有问题,但让一个线程去修改数组在其他线程读取的时候是不安全的。你的单例模式没有避免这种情况发生。 
为了看到问题,看下addPhoto在PhotoManager.m中(已经被替换为如下:)

- (void)addPhoto:(Photo *)photo
{
    if (photo) {
        [_photosArray addObject:photo];
        dispatch_async(dispatch_get_main_queue(), ^{
            [self postContentAddedNotification];
        });
    }
}
 这是一个写操作去修改一个可变的数组。
 现在修改photos属性如下:
- (NSArray *)photos
{
  return [NSArray arrayWithArray:_photosArray];
}
 这个属性的getter方法是一个读方法去访问这个可变数组。这个调用获得一份不可变的拷贝以免被不当破坏,但是没有提供任何保护(当一个线程正在写方法addPhoto时,另外线程去读这个属性)。
 这就是软件系统的读写问题。GCD提供了一个优雅的解决方案通过使用调度障碍来创建读写锁。
 调度障碍(栅栏)是一组函数像在并行队列中的串行式障碍一样。使用GCD阻塞API确保提交的闭包是该特定队列上在特定时间是唯一的被执行元素。这就意味着所有被提交到队列的元素必须在闭包被执行之前完成。
 当轮到该闭包时,阻塞执行闭包确保在这段时间内队列不会执行其他闭包。一旦完成,队列返回到默认实现位置。GCD提供同步和异步两个障碍方法。
 下面的图片说明了障碍函数在各种异步任务中的影响:

这里写图片描述 
请注意如何让正常的操作队列行为就像一个正常的并发队列。但是当障碍执行的时候,它本质上就是一个串行队列。只有该障碍在执行。在障碍完成之后,队列回归为正常的并发队列。 
下面是何时会用,何时不会用: 
1、自定义串行队列:在这里选用很糟糕,因为一个串行队列在任何时候都是仅有一个任务在执行。 
2、全局并发队列:这里注意,不建议选用,因为系统可能正在使用该队列而你不能自己独占他们自己一个人使用。 
3、自定义并发队列:这是一个很好的选择以原子操作的方式去访问临界区。任何你正在设置或初始化的且需要线程安全的都可以选用。 
既然,唯一可以正当选用的选择就是自定义并发队列,你可以创建自己的处理在单独的读和写函数中。并发队列允许同时多个读操作。 
打开PhotoManager.m,添加下面的私有属性到类的补充实现中:

@interface PhotoManager ()
@property (nonatomic,strong,readonly) NSMutableArray *photosArray;
@property (nonatomic, strong) dispatch_queue_t concurrentPhotoQueue; ///< Add this
@end

找到addPhoto:,替换为下面的实现:

- (void)addPhoto:(Photo *)photo
{
    if (photo) { // 1
        dispatch_barrier_async(self.concurrentPhotoQueue, ^{ // 2
            [_photosArray addObject:photo]; // 3
            dispatch_async(dispatch_get_main_queue(), ^{ // 4
                [self postContentAddedNotification];
            });
        });
    }
}
 下面介绍下你的写函数是如何工作的:
      1、在做所有工作之前,确保图片有效。
      2、使用自定的队列去添加写操作。在稍后的时间内在你的队列中该元素将是临界区的唯一执行元素。
      3、这是对象添加到数组的实际代码。因为这是一个障碍闭包,这个闭包将永远不会和其他闭包同时执行在该队列中。
      4、最后你发送了一个通知表明添加了一个图片。这个通知将从主线程发送因为它要处理UI工作。所以这里调度了一个异步任务到主队列去处理通知。
 注意写操作,你也需要实现图片读操作。
 为了确保写入方的线程安全,你需要在该队列中执行读操作。你需要从函数中返回,因此你不能异步调度执行因为那样将导致在读函数返回之前永远不会执行。
 在这种情况下,同步将是一个很好的选择。
 dispatch_sync同步提交任务然后等待直到完成才执行返回。使用dispatch_sync来保持跟踪你的dispatch_barrier工作,或在你可以通过闭包使用数据之前你需要等待操作完成。
 你需要小心。想想一下,如果你调用dispatch_sync然而作用目标即当前队列已经在执行了。这将导致死锁因为调用将等待闭包完成,但是闭包(它甚至不能开始)将在当前正在执行的、不可能结束的闭包结束后才能结束。这将迫使你意识到你正在调用的队列就是你正在传递的队列。
 下面是一个快速的预览何时何地可以使用dispatch_sync:
      1>、自定义串行队列:这种情况下要非常小心,如果你正在运行的一个队列正好是你dispatch_sync的目标队列这将导致死锁。
     2>、 主队列:要小心使用,原因跟上面一样。这种情况也同样存在潜在的死锁。
      3>、并行队列:这是一个不错的选择通过dispatch_barrier或者当等待一个任务完成以便你可以进行下一步操作时。

还在PhotoManager.m,用下面的实现替换属性:

- (NSArray *)photos
{
    __block NSArray *array; // 1
    dispatch_sync(self.concurrentPhotoQueue, ^{ // 2
        array = [NSArray arrayWithArray:_photosArray]; // 3
    });
    return array;
}
 这是一个读函数,依次看注释就会发现:
     1、__block关键字允许在块内部改变该对象,没有这个的话,array在块内将是只读的,你的代码甚至不能通过编译。
      2、队列中的同步调度执行读操作
      3、保存photoArray的拷贝然后返回。

祝贺你,你的PhotoManager单例现在是线程安全的。无论在哪儿或者以何种方式读或写图片,它都将以线程安全的方式毫无意外的正常工作。 
最后,你需要初始化你的并发队列属性。像下面这样改变sharedManager去初始化:

+ (instancetype)sharedManager
{
    static PhotoManager *sharedPhotoManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedPhotoManager = [[PhotoManager alloc] init];
        sharedPhotoManager->_photosArray = [NSMutableArray array];

        // ADD THIS:
        sharedPhotoManager->_concurrentPhotoQueue = dispatch_queue_create("com.selander.GooglyPuff.photoQueue",
                                                    DISPATCH_QUEUE_CONCURRENT);
    });

    return sharedPhotoManager;
}
 使用dispatch_queue_create创建一个并发的队列。第一个参数是反向DNS域名。这样的描述在调试的时候很有用。第二个参数标识队列是串行还是并行。
 注:当在web上查找例子时,你常常看到别人穿0或者NULL作为dispatch_queue_Create的第二个参数。这是一种已经过时的方式,使用具体的(系统提供的枚举类型DISPATCH_QUEUE_CONCURRENT等)作为参数总归是更好的。
 祝贺你,你的PhotoManager单例现在是线程安全的。无论在哪儿或者以何种方式读或写图片,它都将以线程安全的方式毫无意外的正常工作。

视图形式查看排队

 现在是不是还是不能完全掌握GCD的要点?确信可以使用GCD方法创建简单的例子并且使用断点和NSLog去了解正在发生的事情(GCD执行过程中)。
 下面提供了两个GIFs的例子帮助你加强dispatch_async和dispatch_sync的理解。代码以辅助可视化工具的形式包含在GIF图中,注意左边GIF中断点的每一步以及右边关联队列的状态。

dispatch_sync初探:

override func viewDidLoad() {
  super.viewDidLoad()

  dispatch_sync(dispatch_get_global_queue(
      Int(QOS_CLASS_USER_INTERACTIVE.value), 0)) {

    NSLog("First Log")

  }

  NSLog("Second Log")
}

这里写图片描述
下面简要介绍下关系图表: 
1、主队列按序执行,接下来就是一个任务初始化(视图控制器初始化加载viewDidLoad)。 
2、viewDidLoad在主线程执行。 
3、同步闭包被添加到全局队列且稍后执行。进程上表现是主线程停止知道该闭包完成。同时,全局队列是并发执行任务的,在全局队列上是按FIFO的顺序唤起闭包的执行,但是可能是并发执行的。全局队列同时还处理在该同步闭包添加到队列之前就已经存在的任务。 
4、最后,该同步闭包按序执行。 
5、该闭包完成之后,主线程才被唤醒。 
6、viewDidLoad方法执行完毕,主队列接着处理其他任务。 
同步添加任务到队列,然后等待直到该任务完成。异步添加的话,唯一不同的是在被调起的线程里无需等待任务完成就可以继续执行向下执行。

dispatch_async初探:
override func viewDidLoad() {
  super.viewDidLoad()

  dispatch_async(dispatch_get_global_queue(
      Int(QOS_CLASS_USER_INTERACTIVE.value), 0)) {

    NSLog("First Log")

  }

  NSLog("Second Log")
}

这里写图片描述
1、主队列按序执行,接下来就是初始化一个视图控制器任务,viewDidLoad在主线程执行。 
2、viewDidLoad在主线程执行。 
3、主线程现在在viewDidLoad里,刚好执行到dispatch_async。 
4、异步闭包被添加到全局队列,稍后执行。 
5、viewDidLoad在异步闭包添加到全局队列之后继续执行,主线程继续执行未完成的任务。同时,全局队列并发的执行其未完成的任务。记住:全局队列任务是按FIFO顺序出栈执行,但是执行过程中可以是并发的。 
6、被添加的异步闭包正在执行。 
7、异步闭包完成,同时控制台已经有NSLog输出。 
在该热定情况下,第二个NSLog紧随第一个NSLog执行。但并非总是如此,这取决于正在执行的硬件时间,你没有办法知道那个输出先执行。在一些调用中先执行的NSLog可能是第一个NSLog中执行的。

接下来学习什么?

在这个教程中,已经了解了如何使代码线程安全,以及在CPU多任务处理下如何保持主线程的响应能力。 
你可以下载GooglyPuff Project 工程,其中包含了所有目前教程中的实现。第二部分的教程中你将去改进这个项目。 
如果你计划优化你的app,你应该使用Instruments进行性能分析。使用这个工具超出了本教程的范围,所以你应该查阅一些关于如何使用它的优秀文章。 
确保有真机可以使用,因为模拟器测试出来的记过和真实用户使用的体验反馈是不一样的。 
在下一部分(https://www.raywenderlich.com/63338/grand-central-dispatch-in-depth-part-2)的教程中,你将深入的连接GCD的API来做一些更酷的东西。 
如果有问题或者建议,可以自由的加入下面的讨论区。

 ***<第一次做翻译工作,太难了。做的不好,欢迎各位大大们不吝批评指正。万分感谢!>***
***<另外格式也不太好,弄完之后从印象笔记拷出来的,博客的格式编辑有点难搞,就这样将就着看吧。>***
作者:ldz15838245189 发表于2016/8/16 17:30:47 原文链接
阅读:29 评论:0 查看评论

Gson的基础用法

$
0
0
想要使用Gson,首先得导入Gson.jar,网上很多,随便搜一下就可以。
下面是Gson的一些基本的使用方法,我目前能用到的就这么多。
因为我是做Android开发的,最常使用Json的情景,就是客户端发送请求到服务器,服务器返回Json数据,客户端解析为相关的Java类对象,然后展示信息。所以,下面先演示一下用Gson根据一个类生成Json和根据Json串解析为一个类对象:
首先创建一个演示类User:
  1. class User{
  2. String name;
  3. int age;
  4. @SerializedName(value="emailAddress", alternate={"email","email_address"})
  5. String emailAddress;
  6. @Override
  7. public String toString() {
  8. return "User [name=" + name + ", age=" + age + ", emailAddress=" + emailAddress + "]";
  9. }
  10. public User(String name, int age) {
  11. super();
  12. this.name = name;
  13. this.age = age;
  14. }
  15. public User() {
  16. super();
  17. }
  18. }
接着先创建一个Gson对象:
  1. Gson gson = new Gson();
下面就是生成json串的代码:
  1. User user = new User("wcc", 25);
  2. String userString = gson.toJson(user, User.class);
userString的值为:
  1. {"name":"wcc","age":25}
接着将刚生成的Json串再解析为User对象:
  1. // 将Json串解析成User对象
  2. User newUser = gson.fromJson(userString, User.class);
打印出的newUser结果为:
  1. User [name=wcc, age=25, emailAddress=null]

下面说一下注解@SerializedName的使用,它用于解决Json串的字段和相对应的类的字段不匹配的问题。
举个例子:
Json串是这样的:
  1. String jsonString = "{\"name\":\"wcc\",\"age\":25,\"email_address\":\"bj\"}";
而相应类的字段是这样的:
  1. String emailAddress;
如果直接解析结果为:
  1. User [name=wcc, age=25, emailAddress=null]
在字段上加上@SerializedName注解:
  1. @SerializedName("email_address")
  2. tring emailAddress;
结果就变为这样了:
  1. User [name=wcc, age=25, emailAddress=bj]
这还没完,如果Json串里有多个对象,且可能出现多个不同的email字段,如:email、email_address、emailAddress,这种情况可以用@SerializedName的alternate属性解决:
  1. @SerializedName(value="emailAddress", alternate={"email","email_address"})
  2. String emailAddress;

再说说通过TypeToken使用泛型,为什么要使用TypeToken呢?因为Gson是通过反射起作用的,像List<String>.class,List<Integer>.class最后得到的都是同一个字节码List.class,所以通过TypeToken可以区别不同的泛型。
下面通过解析一个Json数组说明:
  1. String jsonArray = "['shenmu','changshengjie','zhetian']";
将上面的数组解析成一个Java数组比较简单,只要这样就行了:
  1. String[] strArr = gson.fromJson(jsonArray, String[].class);
但是想要将其解析成List,就需要借助TypeToken:
  1. List<String> list = gson.fromJson(jsonArray, new TypeToken<List<String>>(){}.getType());
因为TypeToken的Constructor is not visible,所以要用匿名内部类的方式调用,输出list:
  1. [shenmu, changshengjie, zhetian]

好了,基础知识已经具备,下面来解析一个比较复杂的Json:
  1. String jsArray = "[{'boardid': 'tech_bbs'," + "'clkNum': 0," + " 'digest': '',"
  2. + "'docid': 'BUGQFHA500097U7U'}," + "{'boardid': 'tech_bbs'," + "'clkNum': 0," + " 'digest': '',"
  3. + "'docid': 'BUGQFHA500097U7U'}," + "{'boardid': 'tech_bbs'," + "'clkNum': 0," + " 'digest': '',"
  4. + "'docid': 'BUGQFHA500097U7U'}]";
上面的Json数组具有三个内容相同的对象,不管它是什么,反正是一个对象就行了,下面将它解析成一个List,其中的每一个元素都是一个Data对象,首先看Data的定义:
  1. class Data{
  2. String boardid;
  3. int clkNum;
  4. String digest;
  5. String docid;
  6. @Override
  7. public String toString() {
  8. return "Data [boardid=" + boardid + ", clkNum=" + clkNum + ", digest=" + digest + ", docid=" + docid + "]";
  9. }
  10. public Data(String boardid, int clkNum, String digest, String docid) {
  11. super();
  12. this.boardid = boardid;
  13. this.clkNum = clkNum;
  14. this.digest = digest;
  15. this.docid = docid;
  16. }
  17. public Data() {
  18. }
  19. }
上面的定义很好理解,就是声明了一些字段,下面是解析的代码:
  1. List<Data> dl = gson.fromJson(jsArray, new TypeToken<List<Data>>(){}.getType());
一句话搞定,输出:
  1. [Data [boardid=tech_bbs, clkNum=0, digest=, docid=BUGQFHA500097U7U], Data [boardid=tech_bbs, clkNum=0, digest=, docid=BUGQFHA500097U7U], Data [boardid=tech_bbs, clkNum=0, digest=, docid=BUGQFHA500097U7U]]


最后解析一个对我来说很复杂的Json串,非常长:
  1. static String superString = "{"
  2. +"'V9LG4B3A0': ["
  3. +"{"
  4. +"'topicImg':'http://vimg3.ws.126.net/image/snapshot/2016/8/O/N/VBSN1KMON.jpg',"
  5. +"'videosource':'新媒体',"
  6. +"'mp4Hd_url':'http://flv2.bn.netease.com/videolib3/1608/15/jDXFt0488/HD/jDXFt0488-mobile.mp4',"
  7. +"'topicDesc':'好的音乐铸就好的电影,不定期将为大家推荐各类经典影片,一起跟着音乐看电影吧!',"
  8. +"'topicSid':'VBSN1KMOH',"
  9. +"'cover':'http://vimg2.ws.126.net/image/snapshot/2016/8/0/2/VBTJGJQ02.jpg',"
  10. +"'title':'「跟着音乐看电影」第四期:爆裂鼓手',"
  11. +"'playCount': 11575,"
  12. +"'replyBoard':'video_bbs',"
  13. +"'sectiontitle':'',"
  14. +"'replyid':'BTJGJQ01008535RB',"
  15. +"'description':'本期推荐的是2014年上映的剧情音乐电影《爆裂鼓手》,推荐音乐Caravan。',"
  16. +"'mp4_url':'http://flv2.bn.netease.com/videolib3/1608/15/jDXFt0488/SD/jDXFt0488-mobile.mp4',"
  17. +"'length': 304,"
  18. +"'playersize': 0,"
  19. +"'m3u8Hd_url':'http://flv2.bn.netease.com/videolib3/1608/15/jDXFt0488/HD/movie_index.m3u8',"
  20. +"'vid':'VBTJGJQ01',"
  21. +"'m3u8_url':'http://flv2.bn.netease.com/videolib3/1608/15/jDXFt0488/SD/movie_index.m3u8',"
  22. +"'ptime':'2016-08-15 17:20:25',"
  23. +"'topicName':'跟着音乐看电影'"
  24. +"},"
  25. +"{"
  26. +"'topicImg':'http://vimg1.ws.126.net/image/snapshot/2016/3/U/K/VBI02N0UK.jpg',"
  27. +"'videosource':'新媒体',"
  28. +"'mp4Hd_url':'http://flv2.bn.netease.com/videolib3/1608/15/aObgM2410/HD/aObgM2410-mobile.mp4',"
  29. +"'topicDesc':'奇葩事的大盘点。',"
  30. +"'topicSid':'VBI02N0UI',"
  31. +"'cover':'http://vimg1.ws.126.net/image/snapshot/2016/8/7/U/VBTJIF17U.jpg',"
  32. +"'title':'快放假拉横幅了多少时间',"
  33. +"'playCount': 5113,"
  34. +"'replyBoard':'video_bbs',"
  35. +"'videoTopic': {"
  36. +"'alias':'奇葩事的大盘点。',"
  37. +"'tname':'奇葩剧情',"
  38. +"'ename':'T1460515713166',"
  39. +"'tid':'T1460515713166'"
  40. +"},"
  41. +"'sectiontitle':'',"
  42. +"'replyid':'BTJIBJNG008535RB',"
  43. +"'description':'快放假拉横幅了多少时间',"
  44. +"'mp4_url':'http://flv2.bn.netease.com/videolib3/1608/15/aObgM2410/SD/aObgM2410-mobile.mp4',"
  45. +"'length': 120,"
  46. +"'playersize': 1,"
  47. +"'m3u8Hd_url':'http://flv2.bn.netease.com/videolib3/1608/15/aObgM2410/HD/movie_index.m3u8',"
  48. +"'vid':'VBTJIBJNG',"
  49. +"'m3u8_url':'http://flv2.bn.netease.com/videolib3/1608/15/aObgM2410/SD/movie_index.m3u8',"
  50. +"'ptime':'2016-08-15 17:13:44',"
  51. +"'topicName':'奇葩剧情'"
  52. +"}]}";
上面的Json串首先是一个对象,这个对象里有一个数组,数组里又有两个子对象,子对象里又可能含对象,所以我们要定义三个相对应的Java类,首先看最里面的对象对应的类,即节点videoTopic对应的类的定义:
  1. class VideoTopic{
  2. String alias;
  3. String tname;
  4. String ename;
  5. String tid;
  6. @Override
  7. public String toString() {
  8. return "VideoTopic [alias=" + alias + ", tname=" + tname + ", ename=" + ename + ", tid=" + tid + "]";
  9. }
  10. }
再看数组里的对象对应的类的定义:
  1. public class NewsBean {
  2. String topicImg;
  3. String videosource;
  4. String mp4Hd_url;
  5. String topicDesc;
  6. String topicSid ;
  7. String cover;
  8. String title;
  9. int playCount;
  10. String replyBoard;
  11. VideoTopic videoTopic;
  12. String sectiontitle;
  13. String replyid;
  14. String description;
  15. String mp4_url;
  16. int length;
  17. int playersize;
  18. String m3u8Hd_url;
  19. String vid;
  20. String m3u8_url;
  21. String ptime;
  22. String topicName;
  23. @Override
  24. public String toString() {
  25. return "NewsBean [topicImg=" + topicImg + ", videosource=" + videosource + ", mp4Hd_url=" + mp4Hd_url
  26. + ", topicDesc=" + topicDesc + ", topicSid=" + topicSid + ", cover=" + cover + ", title=" + title
  27. + ", playCount=" + playCount + ", replyBoard=" + replyBoard + ", videoTopic=" + videoTopic
  28. + ", sectiontitle=" + sectiontitle + ", replyid=" + replyid + ", description=" + description
  29. + ", mp4_url=" + mp4_url + ", length=" + length + ", playersize=" + playersize + ", m3u8Hd_url="
  30. + m3u8Hd_url + ", vid=" + vid + ", m3u8_url=" + m3u8_url + ", ptime=" + ptime + ", topicName="
  31. + topicName + "]";
  32. }
  33. }
看到上面的类,将VideoTopic对象作为类成员进行声明了,这样解析的时候,相关的节点videoTopic就会被解析为VideoTopic对象并作为对应的NewsBean对象的成员存在。最后看一下最外层对象所对应的类的定义:
  1. public class Wrapper {
  2. List<NewsBean> V9LG4B3A0;
  3. }
因为该对象里面有一个数组,所以我把它定义为List。下面是解析的代码,很简单:
  1. Wrapper w = gson.fromJson(superString, Wrapper.class);
遍历,并输出其中的内容:
  1. List<NewsBean> newsList = w.V9LG4B3A0;
  2. for(NewsBean n : newsList)
  3. System.out.println(n+"\n");
输出结果:
  1. NewsBean [topicImg=http://vimg3.ws.126.net/image/snapshot/2016/8/O/N/VBSN1KMON.jpg, videosource=新媒体, mp4Hd_url=http://flv2.bn.netease.com/videolib3/1608/15/jDXFt0488/HD/jDXFt0488-mobile.mp4, topicDesc=好的音乐铸就好的电影,不定期将为大家推荐各类经典影片,一起跟着音乐看电影吧!, topicSid=VBSN1KMOH, cover=http://vimg2.ws.126.net/image/snapshot/2016/8/0/2/VBTJGJQ02.jpg, title=「跟着音乐看电影」第四期:爆裂鼓手, playCount=11575, replyBoard=video_bbs, videoTopic=null, sectiontitle=, replyid=BTJGJQ01008535RB, description=本期推荐的是2014年上映的剧情音乐电影《爆裂鼓手》,推荐音乐Caravan。, mp4_url=http://flv2.bn.netease.com/videolib3/1608/15/jDXFt0488/SD/jDXFt0488-mobile.mp4, length=304, playersize=0, m3u8Hd_url=http://flv2.bn.netease.com/videolib3/1608/15/jDXFt0488/HD/movie_index.m3u8, vid=VBTJGJQ01, m3u8_url=http://flv2.bn.netease.com/videolib3/1608/15/jDXFt0488/SD/movie_index.m3u8, ptime=2016-08-15 17:20:25, topicName=跟着音乐看电影]
  2. NewsBean [topicImg=http://vimg1.ws.126.net/image/snapshot/2016/3/U/K/VBI02N0UK.jpg, videosource=新媒体, mp4Hd_url=http://flv2.bn.netease.com/videolib3/1608/15/aObgM2410/HD/aObgM2410-mobile.mp4, topicDesc=奇葩事的大盘点。, topicSid=VBI02N0UI, cover=http://vimg1.ws.126.net/image/snapshot/2016/8/7/U/VBTJIF17U.jpg, title=快放假拉横幅了多少时间, playCount=5113, replyBoard=video_bbs, videoTopic=VideoTopic [alias=奇葩事的大盘点。, tname=奇葩剧情, ename=T1460515713166, tid=T1460515713166], sectiontitle=, replyid=BTJIBJNG008535RB, description=快放假拉横幅了多少时间, mp4_url=http://flv2.bn.netease.com/videolib3/1608/15/aObgM2410/SD/aObgM2410-mobile.mp4, length=120, playersize=1, m3u8Hd_url=http://flv2.bn.netease.com/videolib3/1608/15/aObgM2410/HD/movie_index.m3u8, vid=VBTJIBJNG, m3u8_url=http://flv2.bn.netease.com/videolib3/1608/15/aObgM2410/SD/movie_index.m3u8, ptime=2016-08-15 17:13:44, topicName=奇葩剧情]


最后,想要更深入的了解Gson的用法,可以去http://www.jianshu.com/p/e740196225a4看看,我在这里只是梳理一下自己所学。
作者:miyuexingchen 发表于2016/8/16 17:34:47 原文链接
阅读:13 评论:0 查看评论

Zxing图片识别 从相册选取二维码图片进行解析总结

$
0
0

Zxing图片识别 从相册选取二维码图片进行解析总结 

在Zxing扫描识别和图片识别的解析对象是相同的

本文分三个步骤:

1 获取相册的照片

2 解析二维码图片

3 返回结果

 

1) 获取相册照片

google对4.4的uri做了点改动  为了适配多种手机 需要做一个判断版本

在Activity中开启相册:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. Intent innerIntent = new Intent(); // "android.intent.action.GET_CONTENT"  
  2.     if (Build.VERSION.SDK_INT < 19) {  
  3.         innerIntent.setAction(Intent.ACTION_GET_CONTENT);  
  4.     } else {  
  5.        // innerIntent.setAction(Intent.ACTION_OPEN_DOCUMENT);  这个方法报 图片地址 空指针;使用下面的方法
  6. innerIntent.setAction(Intent.ACTION_PICK);  
  7.     }  
  8.   
  9.     innerIntent.setType("image/*");  
  10.   
  11.     Intent wrapperIntent = Intent.createChooser(innerIntent, "选择二维码图片");  
  12.   
  13.     CaptureActivity.this  
  14.             .startActivityForResult(wrapperIntent, REQUEST_CODE);  


选中了照片后返回的数据在onActivityResult方法中获取

 

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. @Override  
  2.     protected void onActivityResult(int requestCode, int resultCode, Intent data) {  
  3.   
  4.         super.onActivityResult(requestCode, resultCode, data);  
  5.   
  6.         if (resultCode == RESULT_OK) {  
  7.   
  8.             switch (requestCode) {  
  9.   
  10.             case REQUEST_CODE:  
  11.   
  12.                 String[] proj = { MediaStore.Images.Media.DATA };  
  13.                 // 获取选中图片的路径  
  14.                 Cursor cursor = getContentResolver().query(data.getData(),  
  15.                         proj, nullnullnull);  
  16.   
  17.                 if (cursor.moveToFirst()) {  
  18.   
  19.                     int column_index = cursor  
  20.                             .getColumnIndexOrThrow(MediaStore.Images.Media.DATA);  
  21.                     photo_path = cursor.getString(column_index);  
  22.                     if (photo_path == null) {  
  23.                         photo_path = Utils.getPath(getApplicationContext(),  
  24.                                 data.getData());  
  25.                         Log.i("123path  Utils", photo_path);  
  26.                     }  
  27.                     Log.i("123path", photo_path);  
  28.   
  29.                 }  
  30.   
  31.                 cursor.close();  
  32.   
  33.                 new Thread(new Runnable() {  
  34.   
  35.                     @Override  
  36.                     public void run() {  
  37.   
  38.                         Result result = scanningImage(photo_path);  
  39.                         // String result = decode(photo_path);  
  40.                         if (result == null) {  
  41.                             Looper.prepare();  
  42.                             Toast.makeText(getApplicationContext(), "图片格式有误"0)  
  43.                                     .show();  
  44.                             Looper.loop();  
  45.                         } else {  
  46.                             Log.i("123result", result.toString());  
  47.                             // Log.i("123result", result.getText());  
  48.                             // 数据返回  
  49.                             String recode = recode(result.toString());  
  50.                             Intent data = new Intent();  
  51.                             data.putExtra("result", recode);  
  52.                             setResult(300, data);  
  53.                             finish();  
  54.                         }  
  55.                     }  
  56.                 }).start();  
  57.                 break;  
  58.   
  59.             }  
  60.   
  61.         }  
  62.   
  63.     }  

 

上面这段代码

 <1> 根据返回的照片信息  获取图片的路径photo_path

 <2> 开启一个解析线程调用解析方法Result result = scanningImage(photo_path);      将photo_path传进去

 <3> 对返回的解析的Result对象进行判断,获取字符串   

 <4> 调用recode对result数据进行中文乱码处理          (  具体在步骤3中说明   )

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. String recode = recode(result.toString());  

 <5>这里我将result 通过setResult(); 返回给了 父Activity   

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1.    

<6> Utils.getPath(getApplicationContext(),  data.getData());    //将图片Uri 转换成绝对路径

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public class Utils {  
  2.   
  3.     public static final boolean isChineseCharacter(String chineseStr) {  
  4.         char[] charArray = chineseStr.toCharArray();  
  5.         for (int i = 0; i < charArray.length; i++) {  
  6.             // 是否是Unicode编码,除了"�"这个字符.这个字符要另外处理  
  7.             if ((charArray[i] >= '\u0000' && charArray[i] < '\uFFFD')  
  8.                     || ((charArray[i] > '\uFFFD' && charArray[i] < '\uFFFF'))) {  
  9.                 continue;  
  10.             } else {  
  11.                 return false;  
  12.             }  
  13.         }  
  14.         return true;  
  15.     }  
  16.   
  17.     /** 
  18.      * Get a file path from a Uri. This will get the the path for Storage Access 
  19.      * Framework Documents, as well as the _data field for the MediaStore and 
  20.      * other file-based ContentProviders. 
  21.      *  
  22.      * @param context 
  23.      *            The context. 
  24.      * @param uri 
  25.      *            The Uri to query. 
  26.      * @author paulburke 
  27.      */  
  28.     public static String getPath(final Context context, final Uri uri) {  
  29.   
  30.         final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;  
  31.   
  32.         // DocumentProvider  
  33.         if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {  
  34.             // ExternalStorageProvider  
  35.             if (isExternalStorageDocument(uri)) {  
  36.                 final String docId = DocumentsContract.getDocumentId(uri);  
  37.                 final String[] split = docId.split(":");  
  38.                 final String type = split[0];  
  39.   
  40.                 if ("primary".equalsIgnoreCase(type)) {  
  41.                     return Environment.getExternalStorageDirectory() + "/"  
  42.                             + split[1];  
  43.                 }  
  44.   
  45.                 // TODO handle non-primary volumes  
  46.             }  
  47.             // DownloadsProvider  
  48.             else if (isDownloadsDocument(uri)) {  
  49.   
  50.                 final String id = DocumentsContract.getDocumentId(uri);  
  51.                 final Uri contentUri = ContentUris.withAppendedId(  
  52.                         Uri.parse("content://downloads/public_downloads"),  
  53.                         Long.valueOf(id));  
  54.   
  55.                 return getDataColumn(context, contentUri, nullnull);  
  56.             }  
  57.             // MediaProvider  
  58.             else if (isMediaDocument(uri)) {  
  59.                 final String docId = DocumentsContract.getDocumentId(uri);  
  60.                 final String[] split = docId.split(":");  
  61.                 final String type = split[0];  
  62.   
  63.                 Uri contentUri = null;  
  64.                 if ("image".equals(type)) {  
  65.                     contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;  
  66.                 } else if ("video".equals(type)) {  
  67.                     contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;  
  68.                 } else if ("audio".equals(type)) {  
  69.                     contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;  
  70.                 }  
  71.   
  72.                 final String selection = "_id=?";  
  73.                 final String[] selectionArgs = new String[] { split[1] };  
  74.   
  75.                 return getDataColumn(context, contentUri, selection,  
  76.                         selectionArgs);  
  77.             }  
  78.         }  
  79.         // MediaStore (and general)  
  80.         else if ("content".equalsIgnoreCase(uri.getScheme())) {  
  81.             return getDataColumn(context, uri, nullnull);  
  82.         }  
  83.         // File  
  84.         else if ("file".equalsIgnoreCase(uri.getScheme())) {  
  85.             return uri.getPath();  
  86.         }  
  87.   
  88.         return null;  
  89.     }  
  90.   
  91.     /** 
  92.      * Get the value of the data column for this Uri. This is useful for 
  93.      * MediaStore Uris, and other file-based ContentProviders. 
  94.      *  
  95.      * @param context 
  96.      *            The context. 
  97.      * @param uri 
  98.      *            The Uri to query. 
  99.      * @param selection 
  100.      *            (Optional) Filter used in the query. 
  101.      * @param selectionArgs 
  102.      *            (Optional) Selection arguments used in the query. 
  103.      * @return The value of the _data column, which is typically a file path. 
  104.      */  
  105.     public static String getDataColumn(Context context, Uri uri,  
  106.             String selection, String[] selectionArgs) {  
  107.   
  108.         Cursor cursor = null;  
  109.         final String column = "_data";  
  110.         final String[] projection = { column };  
  111.   
  112.         try {  
  113.             cursor = context.getContentResolver().query(uri, projection,  
  114.                     selection, selectionArgs, null);  
  115.             if (cursor != null && cursor.moveToFirst()) {  
  116.                 final int column_index = cursor.getColumnIndexOrThrow(column);  
  117.                 return cursor.getString(column_index);  
  118.             }  
  119.         } finally {  
  120.             if (cursor != null)  
  121.                 cursor.close();  
  122.         }  
  123.         return null;  
  124.     }  
  125.   
  126.     /** 
  127.      * @param uri 
  128.      *            The Uri to check. 
  129.      * @return Whether the Uri authority is ExternalStorageProvider. 
  130.      */  
  131.     public static boolean isExternalStorageDocument(Uri uri) {  
  132.         return "com.android.externalstorage.documents".equals(uri  
  133.                 .getAuthority());  
  134.     }  
  135.   
  136.     /** 
  137.      * @param uri 
  138.      *            The Uri to check. 
  139.      * @return Whether the Uri authority is DownloadsProvider. 
  140.      */  
  141.     public static boolean isDownloadsDocument(Uri uri) {  
  142.         return "com.android.providers.downloads.documents".equals(uri  
  143.                 .getAuthority());  
  144.     }  
  145.   
  146.     /** 
  147.      * @param uri 
  148.      *            The Uri to check. 
  149.      * @return Whether the Uri authority is MediaProvider. 
  150.      */  
  151.     public static boolean isMediaDocument(Uri uri) {  
  152.         return "com.android.providers.media.documents".equals(uri  
  153.                 .getAuthority());  
  154.     }  
  155.   
  156. }  


 

2) 解析二维码图片

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. protected Result scanningImage(String path) {  
  2.         if (TextUtils.isEmpty(path)) {  
  3.   
  4.             return null;  
  5.   
  6.         }  
  7.         // DecodeHintType 和EncodeHintType  
  8.         Hashtable<DecodeHintType, String> hints = new Hashtable<DecodeHintType, String>();  
  9.         hints.put(DecodeHintType.CHARACTER_SET, "utf-8"); // 设置二维码内容的编码  
  10.         BitmapFactory.Options options = new BitmapFactory.Options();  
  11.         options.inJustDecodeBounds = true// 先获取原大小  
  12.         scanBitmap = BitmapFactory.decodeFile(path, options);  
  13.         options.inJustDecodeBounds = false// 获取新的大小  
  14.   
  15.         int sampleSize = (int) (options.outHeight / (float200);  
  16.   
  17.         if (sampleSize <= 0)  
  18.             sampleSize = 1;  
  19.         options.inSampleSize = sampleSize;  
  20.         scanBitmap = BitmapFactory.decodeFile(path, options);  
  21.   
  22.   
  23.   
  24.         RGBLuminanceSource source = new RGBLuminanceSource(scanBitmap);  
  25.         BinaryBitmap bitmap1 = new BinaryBitmap(new HybridBinarizer(source));  
  26.         QRCodeReader reader = new QRCodeReader();  
  27.         try {  
  28.   
  29.             return reader.decode(bitmap1, hints);  
  30.   
  31.         } catch (NotFoundException e) {  
  32.   
  33.             e.printStackTrace();  
  34.   
  35.         } catch (ChecksumException e) {  
  36.   
  37.             e.printStackTrace();  
  38.   
  39.         } catch (FormatException e) {  
  40.   
  41.             e.printStackTrace();  
  42.   
  43.         }  
  44.   
  45.         return null;  
  46.   
  47.     }  


3) 返回结果

首先对result判断是否为空  ,如果为空就代表  二维码不标准或者不是二维码图片

在子线程中使用Toast 需要初始化looper 

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. Result result = scanningImage(photo_path);  
  2. // String result = decode(photo_path);  
  3. if (result == null) {  
  4.     Looper.prepare();  
  5.     Toast.makeText(getApplicationContext(), "图片格式有误"0)  
  6.             .show();  
  7.     Looper.loop();  
  8. else {  
  9.     Log.i("123result", result.toString());  
  10.     // Log.i("123result", result.getText());  
  11.     // 数据返回  
  12.     String recode = recode(result.toString());  
  13.     Intent data = new Intent();  
  14.     data.putExtra("result", recode);  
  15.     setResult(300, data);  
  16.     finish();  
  17. }  

 

Result 对象返回的就是二维码扫描的结果   

调用recode(result.toString) 方法进行中文乱码处理     代码如下:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. private String recode(String str) {  
  2.         String formart = "";  
  3.   
  4.         try {  
  5.             boolean ISO = Charset.forName("ISO-8859-1").newEncoder()  
  6.                     .canEncode(str);  
  7.             if (ISO) {  
  8.                 formart = new String(str.getBytes("ISO-8859-1"), "GB2312");  
  9.                 Log.i("1234      ISO8859-1", formart);  
  10.             } else {  
  11.                 formart = str;  
  12.                 Log.i("1234      stringExtra", str);  
  13.             }  
  14.         } catch (UnsupportedEncodingException e) {  
  15.             // TODO Auto-generated catch block  
  16.             e.printStackTrace();  
  17.         }  
  18.         return formart;  
  19.     }  

 

处理好之后将结果字符串 返回给父Activity

 

对于图片识别 有些事项需要注意..

二维码的图标需要保证图片尽量不倾斜    拍照的时候 最好保证手机与二维码 水平

 

附图:

 

 

 

希望对您有用 不足之处请指出  谢谢

资源下载地址:http://download.csdn.net/detail/aaawqqq/7281577

作者:a2241076850 发表于2016/8/16 18:01:11 原文链接
阅读:3 评论:0 查看评论

Android高级之图片加载框架的选择

$
0
0


本文来自http://blog.csdn.net/liuxian13183/ ,引用必须注明出处!


从Android爆发以后,自定义的控件如EditTextWithDelete、ActionBar、PullToRresh逐步进入开发者的视野,想起11年的时候,基本项目中使用的UI还全都是Android提供的基础控件,少一点的动画+布局,下载和网络请求都是用HttpClient,图片加载当然也是下载后再使用,当时的程序资源可没有现在这么丰富,看看Github上开源的项目,现在的程序员应该感到幸福。

项目开发从程序上来讲,万古不变两大事情,一是网络通信框架,二是图片加载框架,有关网络框架上一篇已经介绍了async-http和okhttp,而其他如volly同时拥有网络请求和图片加载两个框架,很多人图省事就一次性使用了,当然facebook自己的开源框架也是写的非常不错,接下来再一一介绍;先贴一张11年我们自己写的imageloader

public class ImageDownloader {

	private static ImageDownloader instance = null;
	private static File cacheDir;
	public static Map<String, SoftReference<Bitmap>> bitMapCache = new HashMap<String, SoftReference<Bitmap>>();

	public static ImageDownloader getInstance() {
		if (instance == null) {
			instance = new ImageDownloader();
		}
		return instance;
	}

	private ImageDownloader() {
		// Find the dir to save cached images
		if (android.os.Environment.getExternalStorageState().equals(
				android.os.Environment.MEDIA_MOUNTED))
			cacheDir = new File(
					android.os.Environment.getExternalStorageDirectory(),
					"WholeMag");
		else
			cacheDir = WholeMagApplication.getInstance().getCacheDir();
		if (!cacheDir.exists())
			cacheDir.mkdirs();
	}

	public void download(String actName, String url, ImageView imageView) {
		BitmapDownloaderTask task = new BitmapDownloaderTask(imageView, actName);
		task.execute(url);
		// return task.doInBackground(url);
	}

	class BitmapDownloaderTask extends AsyncTask<String, Void, Bitmap> {
		// private String url;
		// private boolean flag;
		private final WeakReference<ImageView> imageViewReference; // 使用WeakReference解决内存问题
		private String actName;

		public BitmapDownloaderTask(ImageView imageView, String actName) {
			imageViewReference = new WeakReference<ImageView>(imageView);
			this.actName = actName;
		}

		@Override
		protected Bitmap doInBackground(String... params) { // 实际的下载线程,内部其实是concurrent线程,所以不会阻塞
			Bitmap rebmp = getLocalBitmap(params[0], actName);
			if (rebmp == null)
				rebmp = downloadBitmap(params[0], actName);
			if (rebmp == null) {
				doInBackground(params[0]);
			}
			return rebmp;
		}

		@Override
		protected void onPostExecute(Bitmap bitmap) { // 下载完后执行的
			if (isCancelled()) {
				bitmap = null;
			}
			if (imageViewReference != null) {
				ImageView imageView = imageViewReference.get();
				if (imageView != null) {
					imageView.setDrawingCacheEnabled(true);
					Bitmap temp = imageView.getDrawingCache();
					imageView.setDrawingCacheEnabled(false);
					if (temp != null) {
						temp.recycle();
					}
					double widthX = (float) WholeMagDatas.getDeviceWidth()
							/ bitmap.getWidth(); // 图片宽度拉伸比例
					int bitmapHight = bitmap.getHeight();// 图片高度
					imageView.setImageBitmap(bitmap); // 下载完设置imageview为刚才下载的bitmap对象
					if(actName.equals(AppData.NEWS_DETAIL_ACT)){
						FrameLayout.LayoutParams ll = new FrameLayout.LayoutParams(
								android.view.ViewGroup.LayoutParams.FILL_PARENT,
								(int) (bitmapHight * widthX), Gravity.CENTER);
						imageView.setLayoutParams(ll);
					}
					AlphaAnimation alphaAnimation = new AlphaAnimation(0, 1);// 创建一个AlphaAnimation对象
					alphaAnimation.setDuration(500);// 设置动画执行的时间(单位:毫秒)
					imageView.startAnimation(alphaAnimation);
				}
			}
		}
	}

	static Bitmap getLocalBitmap(String url, String actName) {
		if (bitMapCache.containsKey(url)) {
			return bitMapCache.get(url).get();
		}
		// String tmp = url;
		// String first = url.substring(url.lastIndexOf("/") + 1);
		// tmp = tmp.substring(0, tmp.lastIndexOf("/"));
		// String second = tmp.substring(tmp.lastIndexOf("/") + 1);
		// tmp = tmp.substring(0, tmp.lastIndexOf("/"));
		// String third = tmp.substring(tmp.lastIndexOf("/") + 1);
		// String filename = third + second + first;
		// File f = new File(cacheDir, filename);
		File f = Tools.getFile(actName, url);
		InputStream inputStream = null;
		try {
			// decode image size
			BitmapFactory.Options o = new BitmapFactory.Options();
			o.inPreferredConfig = Bitmap.Config.RGB_565;
			o.inDither = false;
			o.inPurgeable = true;
			// o.inTempStorage = new byte[12 * 1024];
			inputStream = new FileInputStream(f);
			// Bitmap bitmap = BitmapFactory.decodeFile(f.getAbsolutePath());
			Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, o);
			bitMapCache.put(url, new SoftReference<Bitmap>(bitmap));
			return bitmap;
		} catch (Exception e) {
		} finally {
			if (null != inputStream) {
				try {
					inputStream.close();
				} catch (Exception ex) {
				}
			}
		}
		return null;
	}

	static Bitmap downloadBitmap(String url, String actName) {
		final AndroidHttpClient client = AndroidHttpClient.newInstance("linux");
		final HttpGet getRequest = new HttpGet(url);

		try {
			HttpResponse response = client.execute(getRequest);
			final int statusCode = response.getStatusLine().getStatusCode();
			if (statusCode != HttpStatus.SC_OK) {
				// Log.e("cwjDebug", "Error " + statusCode
				// + " while retrieving bitmap from " + url);
				return null;
			}

			final HttpEntity entity = response.getEntity();
			if (entity != null) {
				// String tmp = url;
				// String first = url.substring(url.lastIndexOf("/") + 1);
				// tmp = tmp.substring(0, tmp.lastIndexOf("/"));
				// String second = tmp.substring(tmp.lastIndexOf("/") + 1);
				// tmp = tmp.substring(0, tmp.lastIndexOf("/"));
				// String third = tmp.substring(tmp.lastIndexOf("/") + 1);
				// String filename = third + second + first;
				// File f = new File(cacheDir, filename);
				File f = Tools.getFile(actName, url);
				OutputStream os = new FileOutputStream(f);
				InputStream inputStream = null;
				try {
					inputStream = entity.getContent();
					BitmapFactory.Options o = new BitmapFactory.Options();
					o.inPreferredConfig = Bitmap.Config.RGB_565;
					o.inDither = false;
					o.inPurgeable = true;
					final Bitmap bitmap = BitmapFactory.decodeStream(
							inputStream, null, o);
					bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os);
					bitMapCache.put(url, new SoftReference<Bitmap>(bitmap));
					return bitmap;
				} finally {
					if (inputStream != null) {
						inputStream.close();
					}
					if (null != os) {
						os.close();
					}
					entity.consumeContent();
				}
			}
		} catch (Exception e) {
			getRequest.abort();
		} finally {
			if (client != null) {
				client.close();
			}
		}
		return null;
	}
}
当年为了防止图片爆掉出现OOM,使用了软引用,觉得还不错;图片加载的基本原理就是,把url+imageview抛过来,然后开启异步线程加载,根据获取的byte流decode成bitmap,最后在UI线程将图片加载到imageview上;也没有说做本地缓存,仅做了应用缓存;没有对图片进行压缩或者设置格式,使占用内存更小,展示也更合理;LruCache基本原理跟上面使用软引用的过程差不多,只不过多限制了图片占用内存的大小,计算图片使用的频率,对应用层、SD卡层均做了封装。

上面介绍完,相信你也对图片加载有个大概的轮廓,我们拿开源的imageloader为例,来讲讲图片加载框架的一些细节

public final class ImageLoaderConfiguration {

    final Resources resources;//主要给图片设计宽高时,获得屏幕宽高使用
    final int maxImageWidthForMemoryCache;//内存中最大的图片宽度
    final int maxImageHeightForMemoryCache;//内存中最大的图片高度

    final int maxImageWidthForDiskCache;//SD卡中最大的图片宽度
    final int maxImageHeightForDiskCache;//SD卡中最大的图片高度
    final BitmapProcessor processorForDiskCache;//从SD卡获得Bitmap的加载器

    final Executor taskExecutor;//加载图片时的执行器
    final Executor taskExecutorForCachedImages;//加载缓存时的执行器
    final boolean customExecutor;//是否使用默认执行器
    final boolean customExecutorForCachedImages;//是否使用默认缓存执行器

    final int threadPoolSize;//线程数,可以用来控制展示当前界面的item图片
    final int threadPriority;//线程的执行优先级
    final QueueProcessingType tasksProcessingType;//是LILO还是LIFO,默认是前者,但一般喜欢后者

    final MemoryCache memoryCache;//内存缓存对象,如不写可用默认
    final DiskCache diskCache;//SD卡缓存对象,如不写可用默认
    final ImageDownloader downloader;//图片加载器,根据网络(http/s)、file、content、drawable、asset来加载
    final ImageDecoder decoder;//图片解析器,根据获取的图片参数拿到Bitmap
    final DisplayImageOptions defaultDisplayImageOptions;//设置图片加载状态和结果,见下面源码

    final ImageDownloader networkDeniedDownloader;//不用网络下载图片的下载器,可理解为加载SD卡图片的加载器
    final ImageDownloader slowNetworkDownloader;//仅网络下载图片的下载器,支持断点续传

拿这些变量来讲,基本就可以说明事情 
public final class DisplayImageOptions {

    private final int imageResOnLoading;//图片是否加载中
    private final int imageResForEmptyUri;//图片是否来自于空url
    private final int imageResOnFail;//图片是否加载失败
    private final Drawable imageOnLoading;//加载中的图片
    private final Drawable imageForEmptyUri;//空数据的图片
    private final Drawable imageOnFail;//加载失败的图片
    private final boolean resetViewBeforeLoading;//加载完是否重置(意味着放弃之前的加载)
    private final boolean cacheInMemory;//是否缓存在内存中
    private final boolean cacheOnDisk;//是否缓存在SD卡中
    private final ImageScaleType imageScaleType;//要多大的图片,统一设置
    private final Options decodingOptions;//Bitmap的options对象
    private final int delayBeforeLoading;//是否延迟加载,可用于非当前页面图片
    private final boolean considerExifParams;//是否支持jpeg图片的rotate和flip等方法
    private final Object extraForDownloader;//额外数据
    private final BitmapProcessor preProcessor;//加载不在内存中的图片
    private final BitmapProcessor postProcessor;//加载在图片中的图片
    private final BitmapDisplayer displayer;//展示图片
    private final Handler handler;//这个就不用讲了吧,跟主线程交互必不可少的工具
    private final boolean isSyncLoading;//是否同步加载 

补充一下,上面框架还支持给图片设置像素点占位大小;看到这么多功能,对于现在的项目基本满足要求,因此就不打算换了,再看看其他几种图片加载框架的异同

fresco,facebook出品,最大的优势在于可展示加载过程,即加载进度、加载前图片、加载中图片、加载后图片、加载失败图片等,还可以设置图片的形状

picasso,加载更快,因为默认设置图片格式占内存小

Glide是升级版本的piccaso,支持跟fragment和activity生命周期绑定

volly,基于老的imageloader又做了次封装,差别不是太大,功能弱化一些

而后面这几种框架,都是在imageloader兴起之后出现的,所以也基本支持它,在我看来也仅有fresco和glide是真正写出了跟原框架不同的东西。


作者:liuxian13183 发表于2016/8/16 18:40:40 原文链接
阅读:11 评论:0 查看评论

androidstudio的UIautomaster界面自动化测试

$
0
0

Android测试支持库包含 UI自动化模块 ,它可以对Android应用进行自动黑盒测试。在API Level 18中引入了自动化模块,它允许开发者在组成应用UI的控件上模仿用户行为。

在这个教程中,我将展示如何使用此模块来创建和执行一个基本的UI测试,选择默认的计算器模块进行测试。

先决条件

在使用前,需要具备以下条件:

  1. 最新版本的 Android Studio
  2. 运行Android 4.3或者更高版本的设备或者虚拟器
  3. 理解 JUnit

1. 安装依赖库

工程中使用UI自动化模块,需要编辑你的工程下 app 目录下的文件 build.gradle ,添加如下信任:

androidTestCompile 'com.android.support.test:runner:0.2'                       
  androidTestCompile 'com.android.support.test:rules:0.2'                        
  androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.0'

现在屏幕上应该有 Sync Now 按钮了,但点击它时,会看到如下错误信息:

Android UI 自动化测试

点击 Install Repository and sync project 链接来安装 Android Support Repository。

如果使用的是库 appcompat-v7 且其版本号是 22.1.1 ,你需要添加如下依赖以确保应用本身和测试应用都使用相同版本的com.android.support:support-annotations:

androidTestCompile 'com.android.support:support-annotations:22.1.1'

接下来,由于Android Studio自身的一个bug,你需要通过packagingOptions执行一个名为 LICENSE.txt 的文件。这个执行失败的话,在运行测试时将引起如下错误:

Execution failed for task ':app:packageDebugAndroidTest'.                                                                                   
   Duplicate files copied in APK LICENSE.txt                                                                                                   

   File 1: ~/.gradle/caches/modules-2/files-2.1/org.hamcrest/hamcrest-core/1.1/860340562250678d1a344907ac75754e259cdb14/hamcrest-core-1.1.jar  
   File 2: ~/.gradle/caches/modules-2/files-2.1/junit/junit-dep/4.10/64417b3bafdecd366afa514bd5beeae6c1f85ece/junit-dep-4.10.jar

在你的 build.gradle 文件底部增加如下代码段:

android {                                       
       packagingOptions {                          
           exclude 'LICENSE.txt'                   
       }                                           
   }

2、创建测试类

创建一个新的测试类,CalculatorTester,通过在 androidTest 目录下创建名为 CalculatorTester.java 的文件实现。创建的UI自动化测试用例,必须继承自InstrumentationTestCase。

Android UI 自动化测试

按 Alt+Insert后选择 SetUp Method 来重写setUp方法。

Android UI 自动化测试

再次按 Alt+Insert 后选择 Test Method 来生成新的测试方法,命名为testAdd。到此CalculatorTester类定义如下:

public class CalculatorTester extends InstrumentationTestCase{

      @Override                                                 
      public void setUp() throws Exception {                    

      }                                                         

      public void testAdd() throws Exception {                  

      }                                                         
  }

3、查看Launcher UI

连接你的Android设备到电脑商,点击home按键,进入主界面。

返回到你的电脑,使用文件管理或者终端浏览你安装Android SDK的目录,进入到 tools 目录下,点击 uiautomatorviewer 。这个会启动 UI Automater Viewer ,你将看到如下界面:

Android UI 自动化测试

点击上方手机图标来获取Android设备截屏。注意到此时获取到的截屏是可交互的。点击下面的Apps图标。在右方的 Node Detail 区域,你就可以看到根据选择图标的不同显示不同的详细信息,如下图所示:

Android UI 自动化测试

与屏幕上的应用交互,UI自动化测试需要能唯一识别它们。在这个教程中,可以使用应用的textcontent-desc或者class字段来唯一的区分。

从上图可以看到Apps图标没有text字段,但有content-desc。记下它的值,后面将用到这个值。

拿起Android设备,触摸Apps图标,进入设备安装的所有应用界面。使用 UI Automater Viewe 获取另外一张屏幕截图。因为要写一个计算器应用的测试,点击计算器图标查看详细界面。

Android UI 自动化测试

这次content-desc是空的,但是text的值为Calculator,同样记住这个值。

如果你的Android设备运行不同的主界面或者不同的Android版本,界面和显示的细节会有所不同。这意味着后续代码中需要做一些修改,以匹配你的操作系统。

4、准备测试环境

返回到Android Studio,给setUp方法中添加代码。如同其名字,setUp方法是用来准备测试环境的。换句话说,这个方法是在真正测试之前指定具体需要执行什么动作的。

现在需要写代码来模拟刚才在Android设备上执行的几个动作:

1、按home键进入主界面

2、按Apps图标进入应用界面

3、点击计算器图标启动它

在你的类中声明类型为UiDevice的变量device。它代表你的Android设备,后续使用它来模拟用户行为。

private UiDevice device;

在setUp方法中,通过调用UiDevice.getInstance method来初始化device,传递Instrumentation实例,如下所示:

device = UiDevice.getInstance(getInstrumentation());

模拟点击设备home键,需要调用pressHome方法。

device.pressHome();

接下来,需要模拟点击Apps图标的动作。不能立即做这个动作,因为Android设备需要一个反应时间来加载界面。如果在屏幕显示出来之前执行这个动作就会引起运行时异常。

等待一些事情发生时,需要调用UiDevice实例的wait方法。等待Apps图标显示到屏幕,使用Until.hasObject方法。

识别Apps图标需要使用By.desc方法并传递值为 Apps 的参数。你还需要指定最长等待时间,单位为毫秒。此处设置为3000。

至此形成如下代码段:

// Wait for the Apps icon to show up on the screen
  device.wait(Until.hasObject(By.desc("Apps")), 3000);

要获取Apps图标的引用,需要使用findObject方法。一旦有了Apps图标的引用,就可以调用click方法来模拟点击动作了。

UiObject2 appsButton = device.findObject(By.desc("Apps"));
  appsButton.click();

和前面一样,我们需要等待一些时间,保证计算器图标显示到屏幕上。在之前的步骤中,我们看到可以通过text字段唯一的识别计算器图标。我们调用By.text方法来找到图标,传递参数为Calculator。

// Wait for the Calculator icon to show up on the screen
  device.wait(Until.hasObject(By.text("Calculator")), 3000);

5、检查计算器UI

在你的Android设备上启动计算器应用,使用 UI Automater Viewer 来查看显示。获取到一个截屏后,点击不同的按钮来观察使用何值可以唯一的区分它们。

在本次测试用例中,使用计算器计算 9+9= 的值并确认结果是否为 18 。这意味着你需要知道怎么区分按键 9+=

Android UI 自动化测试

在我的设备上,如下是我收集到的信息:

  1. 数字按键匹配text值
  2. += 使用content-desc值,分别对应 plusequals
  3. 返回值显示在EditText控件中

如果你使用不同版本的计算器应用,请注意这些值有可能不一样。

6、创建测试类

在前面几步操作中,你已经学会了使用findObject方法通过By.text或者By.desc来获取屏幕上不同对象的引用。还学会了通过click方法来模拟点击对象的动作。下面的代码使用这些方法来模拟 9+9= 。添加这些到类CalculatorTester的方法testAdd中。

// Wait till the Calculator's buttons are on the screen        
  device.wait(Until.hasObject(By.text("9")), 3000);              

  // Select the button for 9                                     
  UiObject2 buttonNine = device.findObject(By.text("9"));        
  buttonNine.click();                                            

  // Select the button for +                                     
  UiObject2 buttonPlus = device.findObject(By.desc("plus"));     
  buttonPlus.click();                                            

  // Press 9 again as we are calculating 9+9                     
  buttonNine.click();                                            

  // Select the button for =                                     
  UiObject2 buttonEquals = device.findObject(By.desc("equals")); 
  buttonEquals.click();

现在就等待运行结果。此处不能使用Until.hasObject,因为包含计算结果的EditText已经显示在屏幕上了。取而代之,我们使用waitForIdle方法来等待计算完成。同样,最长等待时间是3000毫秒。

device.waitForIdle(3000);

使用findObject和By.clazz methods方法获取EditText对象的引用。一旦有了此引用,就可以调用getText方法来确定计算结果是否正确。

UiObject2 resultText = device.findObject(By.clazz("android.widget.EditText"));
  String result = resultText.getText();

最后,使用assertTrue来检验范围值是否为 18

assertTrue(result.equals("18"));

测试到此结束。

6、执行测试

执行测试,需要在Android Studio的工具栏中选择CalculatorTester,点击它右方的 play 按钮。

Android UI 自动化测试

一旦编译结束,测试就成功运行完整。当测试运行时,在你的Android设备上就会看到UI自动化运行界面。

Android UI 自动化测试

总结

在这篇教程中,我们学会了如何使用UI自动化测试模块和 UI Automater Viewer 来创建用户界面测试。你也看到了使用Android Studio执行测试是如此简单。虽然我们测试了一个相对简单的应用,但可以将从中学到的概念用到几乎所有Android应用的测试中。

作者:qq_30953277 发表于2016/8/17 15:45:11 原文链接
阅读:64 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>