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

深入Activity,Activity启动模式LaunchMode完全解析

$
0
0

转载请注明出处:http://blog.csdn.net/linglongxin24/article/details/53221384
本文出自【DylanAndroid的博客】


深入Activity,Activity启动模式LaunchMode完全解析

在平时的开发中,我们可能会了解到Activity的任务栈还有Activity的启动模式。那么Activity的启动模式都分别是怎么样的呢?如果设置了这些启动模式对任务栈有事么影响
,还有就是这么启动模式在实际的开发中有什么应用呢?这里用图例和demo来分析一下Activity的启动模式。

Demo图

1.Standard:标准启动模式

Activity的默认模式,所有的Activity元素遵循先进后出的进栈出栈的特性,这种的比较简单

启动顺序:A->B->C

回退顺序:C->B->A.

Standard

2.SingleTop:栈顶复用模式

栈顶复用模式,如果想要打开的activity在任务栈的栈顶已经存在,就不会创重新建新的实例,而是调用该Activity的 onNewIntent() 方法。避免栈顶的activity被重复的创建。

例如A.B启动模式为Standard,C启动模式为SingleTop

启动顺序:A->B->C—>C

回退顺序:C->B->A.而不是C->C->B->A

SingleTop
应用如下:
* 点击通知栏重复打开Activity的问题
全新的Android通知栏,已抛弃setLatestEventInfo,兼容高版本 这篇文章里面
我们打开一个通知栏,点击通知栏默认打开MainActivity,有一个问题,就是如果不设置MainActivity的launchMode,就会每次点击通知栏的时候会重新打开一个Activity。
我们可以将MainActivity的启动模式设置为SingleInstance,就不会再重新打开MainActivity,而是调用MainActivity的onNewIntent() 方法。
* 可以解决按钮重复点击的问题(当然这种启动模式不是为了去解决这个问题在这里这是说为了用这么应用去说明SingleTop启动模式)。

3.SingleTask:栈内复用模式

如果想要启动的Activity在当前栈内启动之后,该activity只会在任务栈里面存在一个实例。如果要再次打开这个activity,在任务栈里面如果已经存在,就不会创建新的activity,
而是复用栈内的这个已经存在的activity,调用改Activity的 onNewIntent() 方法,并且清空这个activity任务栈上面所有的activity。

例如A.C.D启动模式为Standard,B启动模式为SingleTask

启动顺序:A->B->C—>D—>B

回退顺序:B->A.而不是B—>D->C->B->A

SingleTop
应用如下:如果从主页去登录,然后去注册,注册完成如果直接回去主页,可以将主页的launchMode设置为SingleTask。直接从注册跳到主页即可,不用去关心LoginActivity是否关闭,还是什么时候关闭。

4.SingleInstance:单一实例模式

在整个Android系统中(可能会有很多任务栈)里面只有一个实例存在。不同的应用程序app去启动这个activity,就会共享公用同一个activity。
他会运行在自己单独的的任务栈里面,并且这个单独的任务栈里面只会存在着一个实例。而且这个单独的任务栈会在最底层。
应用场景:系统的发短信,打电话,来电,浏览器等。这种模式在平时很少去使用,一般在Launcher中可能会用到。

例如A.C启动模式为Standard,B启动模式为SingleInstance

启动顺序:A->B->C;注意:此时产生了两个任务栈,B产生了一个新的任务栈,并处于其他任务栈的下面。

回退顺序:C->A->B.而不是C->B->A

SingleTop

5.GitHub

作者:u010785585 发表于2016/11/18 19:14:35 原文链接
阅读:192 评论:0 查看评论

Activity(Intent-filter详解及跳转)

$
0
0

转载请注明出处:http://blog.csdn.net/huiblog/article/details/53221809

五壮士之 Activity

  • 开场白

    为什么说是五壮士呢?
    Android知识体系中最重要的就是四大组件,也就是ActivityServiceContentProvider以及BrocastRecever。还有一个重要的组件Application。所以,四大组价+Application=五壮士。
    这重要的五个家伙就跟人体的心、肝、脾、肺、肾一样,个个组件之间分工明确,构成了我们强大的Android体系。这里我们简单的了解一下就可以了,后续文章我们会一一聊聊的。
    这是Activity系列文章第一节。

什么是Activity

  • 概论

    Activity就像人的脸面一样,这个玩意承载着一个APP的单独的一个门面。通过这个组件,我们可以让APP当前的页面中显示任何我们想要显示的东西。例如:我们看到的文字(可能是Textview这个控件)、一个按钮(可能是Button 这个控件)、
    一张图片(可能是ImageView 这个控件)。

    为什么说可能呢?因为Android的控件是很灵活的,所看到的的不一定是真的,你看到的是按钮(Button),可能他是使用图片+文字组合在一起的。(当然,后续文章中我们会知道,Button的实质就是图片+文字组合在一起的)

  • UI构成简介

    其实Activity并不是在对象里面添加了一个部局文件那样简单。
    我们所用的布局文件其实是通过PhoneWindow放到了DecorView的mContentParent里面,最终形成了我们看到的。

    • 简单图示

    Activity UI简单图示
    这里就放个图在这,简单看看就行。Activity的后续文章中我会进行详解的。

Activity的跳转

我们新建一个Activity后,需要在AndroidManifest.xml这个重要的文件中进行注册我们的Activity,为什么要在这个文件中注册呢?因为这个文件相当于一个APP的简历,通过这个简历我们可以知道这个APP想要什么权限、有哪些需要展示的页面、能接受什么广播、有哪些服务、能向外界提供哪些数据,等等。
使用Android Studio 创建一个默认的APP,我们看看这个AndroidManifest.xml里都写了什么。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.asia.testactivity">
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" /> 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity> 
    </application>
</manifest>

捡当前我们讲的跳转来看,<activity android:name=".MainActivity">这个表示MainActivity是一个Activity。而<intent-filter> 是意图过滤器在他的子节点的两行代码<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
表示当前所在的父节点的Activity是主Activity,也就是说打开APP时,系统会默认打开这个Activity也就是这段XML中的MainActivity。具体intent-filter是干嘛的后面会讲的。

Activity的跳转呢有两种跳转方法,分别是显示跳转隐式跳转

  • 显示跳转
    显示跳转就是说很明显我们知道要跳转到哪一个Activity的意思。
    Demo:
Intent intent = new Intent();
intent.setClass(context, TargetActivity.class);
startActivity(intent);

执行上面的一段代码后,我们就能跳转到到指定的TargetActivity了。
另一种写法:

Intent intent =  new Intent(context, TargetActivity.class);
startActivity(intent);

两种意思都一样,指定了目的Activity,使用Intent跳转。

  • 隐式跳转

隐式跳转就像一种模糊跳转,就跟上网买衣服差不多,过滤条件写着冬季、蓝色、175cm的大衣,电商就列了一列衣服供你选择。隐士跳转呢,我们指定要跳转的Activity需要什么样的条件,系统会将系统中所有的应用的AndroidManifest.xml遍历一遍找到你提出条件的Activity,然后跳转到这个Activity中。如果有多个符合条件的Activity,就弹窗问你需要跳转到哪一个,如果没有找到符合条件的Activity,系统就会骂你,傻啊你,没有你这么苛刻条件的Activity,然后就抛异常了。O(∩_∩)O哈哈~

Demo主要代码
MainActivity

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.e("hui", "MainActivity onCreate");
    }

    public void onClick(View view){
        Intent intent = new Intent();
        intent.setAction("my.custom.action");
        intent.addCategory("android.intent.category.DEFAULT");
        intent.setData(Uri.parse("mydata:lalalala"));
        startActivity(intent);
    }

}

AndroidManifest.xml中SecondActivity的注册代码

 <activity android:name=".SecondActivity">
            <intent-filter>
                <action android:name="my.custom.action" />
                <data android:scheme="mydata"/>
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
</activity>

效果:
代码结果

上面的例子看一下,下面就详细讲解一下这个intent-filter强大的过滤功能,最后在看这个例子就明白了。

intent-filter (意图过滤器)

有三个重要的参数,action、category、data。

  • action 可以翻译为功能,这个东西呢谷歌提供的是给系统用的,我们开发APP时由我们开发人员自定义的,代表着我们自己定义的这个Activity拥有什么样的功能。
  • category 类别,上面写的”android.intent.category.DEFAULT”就是一般情况下用的默认类别。
    category是使用系统给我们提供的,有很多种,例如:
    category举例
    不同的category代表着Activity的不同类别,比如第一个“”android.intent.category.BROWSABLE”代表着这是浏览器类别的。
    其他的常用action及category参考这个
  • data 这个东西的官方意思是“表示操作数据的URI和MIME类型”,也就是说包含了当前action所必须的数据规范。举例来说:我们调用系统的打电话的功能(setAction(Intent.ACTION_CALL))就必须要传递过去一个电话号码(setData(Uri.parse(“tel:110”))),上面两个设置后就可以给110打电话了(当然需要权限)。如果没有电话号码调用打电话的功能没意义的。 data有两个属性:

    • 属性一:scheme
      上例子中,<data android:scheme="mydata"/>,那么如果你想打开我这个Activity就必须给我一个以mydata为前缀开头的数据,例如上面的intent.setData(Uri.parse(“mydata:lalalala”));我们获取到这个数据就可以做我们自己的操作。

      对于Intent#setData方法,public Intent setData(Uri data) 需要一个 uri,我们通过 public static Uri parse(String uriString) 就可以将“mydata:lalalala”传递给目的Activity了。
      如果有多个scheme,例如有两个scheme约束:<data android:scheme="mydata"/><data android:scheme="otherdata"/> 我们的setData方法只需要匹配其中任意一个即可,使用intent.setData(Uri.parse("mydata:xxxxx")) 或者 intent.setData(Uri.parse("otherdata:xxxx")):其中任意一个都可以与这个intent-filter匹配

    • 属性二:mimeType
      这个东西代表了传递的数据类型,比如mp4类型(audio/mpeg),3gp类型(audio/3gpp),等等。注意:格式必须是XXX/XXX类似的。使用public Intent setType(String type) 就可以了。

      注意!
      在同时有setData和setType时,这个东西互斥的,不能同时生效。必须只能使用public Intent setDataAndType(Uri data, String mimeType) 。不知道谷歌当初怎么想的^(* ̄(oo) ̄)^。

现在看上面隐士跳转的demo代码是不是明白了许多,↖(^ω^)↗。
在SecondActivity的注册代码中,intent-filter的子节点action和category代表着这个Activity是 “android.intent.category.DEFAULT”类型的Activity,拥有”my.custom.action”功能的Activity,同时需要mydata前缀的数据。
而在MainActivity 的隐式跳转中,系统根据指定的action和category以及data的scheme遍历全部应用AndroidManifest.xml文件,发现与SecondActivity的约束条件一致,完全匹配,而且只有这一个,就打开了SecondActivity。

完全匹配的指的是,我如果要求身高175、体重50kg以上这条件,那么相亲对象就必须符合这两个条件。在代码中,意思是你必须符合我Intent-filter中的所有约束条件,才是匹配我的资格。还拿上面SecondActivity例子说明,SecondActivity的intent-filter中有action、category和data三个约束,那么你给我的intent中必须也有三个对应的约束,才能匹配。如果我的intent中只有action和category这两个约束值,那么两者是不匹配的,完全一致才匹配。

Activity跳转流程

借用Google的图示,加深理解。
Intent跳转
无论隐式跳转还是显示跳转,流程都如上图所示,Activity A创建一个包含要跳转到目的Activity信息的Intent,然后通过startActivity将intent数据发给系统,[2]系统拿到intent后进行解析intent中数据,查询手机中所有的APP中的AndroidManifest.xml文件中的intent-filter,找与之匹配的intent-filter,【3】当找到了与之匹配的intent-filter后就打开这个Activity。找不到,就抛异常(^o^)/~

最后,看下如果有多个符合约束条件的Activity会怎么样呢?
新建第三个Activity,注册的intent-filter如下:

<activity android:name=".ThridActivity">
            <intent-filter>
                <action android:name="my.custom.action" />
                <data android:scheme="mydata"/>
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

多个完全匹配的Activity中Intent-filter

全部代码:http://download.csdn.net/detail/huiblog/9686973

作者:huiblog 发表于2016/11/18 19:30:28 原文链接
阅读:11 评论:0 查看评论

自定义控件01

$
0
0

1.AutoCompleteTextView

1 回顾Android中原生控件
1) autoCompleteTextview 这个控件展示数据和listview一样

2)listview展示数据的原理 : 需要一个适配器(arrayAdapter baseAdapter simpleadapter …)

3)实现代码 completionThreshold=”1”输入一个数就能搜索

3.1)在布局里面声明 
 <AutoCompleteTextView 
    android:id="@+id/actv" 
    android:layout_width="match_parent"
    android:completionThreshold="1"
    android:layout_height="wrap_content" />

3.2)创建适配器 展示数据

    setContentView(R.layout.activity_main);
    private static final String[] COUNTRIES = new String[] {
         "Belgium", "France", "Italy", "Germany", "Spain"
     };

    //[1]找到控件
    AutoCompleteTextView actv = (AutoCompleteTextView) findViewById(R.id.actv);
    //[2]actv这个控件展示数据 原理和listview展示数据的原理一样需要一个适配器 
    ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
            android.R.layout.simple_dropdown_item_1line, COUNTRIES);
    //[3]设置适配器
    actv.setAdapter(adapter);

2.button和imagebutton

3.自定义控件的分类

[1]通过Android中原生的控件进行组合 来达到自定义的需求

[2]纯自定义控件  

    1)定义类继承View 

    2)定义类继承ViewGroup 

[3]View&ViewGroup    

     view是用户构建界面的基本模块 

     view在屏幕上占据的是矩形区域

     view的职责是绘制和事件的处理

     ViewGroup也继承View

     ViewGrtoup可以包含自己的孩子 

     平常我们使用的常见布局 比如 线性 相对 绝对 帧布局都直接继承ViewGroup 

4.下拉选择框

功能的分析 

 1)通过edittext 和 button  和popupWindow 和listview 的一个组合 达到需求 

 2)当点击按钮 弹出一个popupWindow  popupwindow里面展示的内容是由listview来填充

 3)当点击listview的条目 把该条目的内容取出来展示到edittext上 

 4)当点击listview 条目上的删除按钮可以把对应的条目的内容删除

实现步骤

[1]画Ui

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    tools:context=".MainActivity" >

    <EditText
        android:id="@+id/et_number"
        android:layout_width="200dp"
        android:layout_height="wrap_content" />

    <ImageButton
        android:id="@+id/ib_down_arrow"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignRight="@id/et_number"
        android:background="@null"
        android:src="@drawable/down_arrow" />

</RelativeLayout>

[2]根据我们画的ui 写对应的逻辑 首先点击按钮 弹出popupwindow

    ListView listview =  initListView();

    /**
     * contentView popupwindw要展示的内容
     * width :popupwindo的宽 
     * true :代表popupwindow可以获取焦点
     */
    //[1]初始化popupwindow
    if (mpopupWindow == null) {
        mpopupWindow = new PopupWindow(listview, et_number.getWidth()-7, 250, true);
        //[1.1]点击popupwindow外部消失 
        mpopupWindow.setOutsideTouchable(true);
        mpopupWindow.setBackgroundDrawable(new ColorDrawable());


    }
    //[2]展示popuowindow  参数1 在哪个view下面  xoff:x轴偏移量  
    mpopupWindow.showAsDropDown(et_number, 4, -4);

[3]popupwindow实际展示的内容是listview的内容,初始化listview的内容

/**初始化listview的方法**/
private ListView initListView() {
    //[1]通过打气筒把一个布局转换成一个listview
    ListView listView = (ListView) View.inflate(getApplicationContext(), R.layout.listview, null);
    //[]设置listview的分割线 
    listView.setDivider(new ColorDrawable(Color.GRAY));
    listView.setDividerHeight(1);
    //[2]listview展示数据 创建适配器 
    listView.setAdapter(new MyAdapter());
    //[3]给listview条目设置点击事件 
    listView.setOnItemClickListener(new OnItemClickListener() {

        @Override
        public void onItemClick(AdapterView<?> parent, View view,
                int position, long id) {
            //[4]取出点击条目的内容  数据在哪里 存着就去哪里取 
            String data = lists.get(position);
            //[5]把data的数据展示到edittext上
            et_number.setText(data);
            //[6]让popupwindow消失 
            mpopupWindow.dismiss();         
        }
    });

    return listView;
}

[4]给listview条目设置点击事件,★ 当listview的条目上有button imageButton checkBox控件的时候会抢占listview条目点击事件的焦点,

 解决方案:

 1)我们不使用button等控件 换其他控件

 2)在listview条目的根上加如下属性 

1.android:descendantFocusability="blocksDescendants"

[5] 给listview条目上的删除按钮设置点击事件

ib_delete.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {

                //[5]把点中条目先从集合里面删除 
                lists.remove(position);
                //[6]通知适配器更新
                notifyDataSetChanged();

        }
        });

5.View绘制的流程

测量 ——–> 排版(布局) —–> 绘制

Measure ——->layout——->draw

★测量: 一般我们重写onMeasure方法

measure方法由于是final的所以不可以重写 ,我们发现有一个onMeasure方法 
那我就重写onMeasure方法,实际的测量工作是在onMeasure方法里面完成 

我们需要在onMeasure方法里面调用setMeasuredDimension 完成控件的测量

/**
 * widthMeasureSpec:父容器对myView宽度的期望  (layout_width)
 * heightMeasureSpec:父容器对myView高度的期望 (layout_height)
 * 参数1和2 实际上是由2部分组成  mode(模式) + size(具体控件大小) 组成 
 *   模式分三种
 *   [1]UNSPECIFIED 为指定 爹不管模式 父容器对孩子没有约束 
 *   [2]EXACTLY 精确的  父容器对孩子的大小有限制  布局的参数是一个具体的值 或者match_parent
 *   [3]AT_MOST 父容器对孩子的最大值有要求 对应布局中wrap_content
 *   
 *   我们在布局中声明的200dp宽和高 是如何传递到onMeasure方法里面的??
 *   [1]首先孩子像父容器申请200dp的宽layout_width和200dp的高layout_height ,父容器拿着孩子的申请打包成对孩子的期望widthMeasureSpec,heightMeasureSpec.
 *      调用孩子的measure方法  ---->会调用onMeasure方法 
 *   
 */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int mode = MeasureSpec.getMode(widthMeasureSpec);
    if (mode ==MeasureSpec.EXACTLY ) {
        //获取大小 
        int size = MeasureSpec.getSize(widthMeasureSpec);
        System.out.println("size:"+size);
    }
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

}

排版 layout

 l t r b 参数 如下图 

通过layout方法底层调用setFrame方法完成对view的上下左右进行赋值.

当我们在定义一个类继承View 的时候一般不需要重写此方法,因为view摆放的位置是由父容器发起的 

 draw方法实现对view绘制,我们在实际开发中一般会重写onDraw方法往当前的view上画内容 不会重写draw,先绘制背景,在绘制内容

 当绘制内容的时候需要重写onDraw();

6.绘制的实战

1)画线

canvas.drawLine(10, 10, 40, 40, paint);

2)画圆

// 使圆圆滑,没有锯齿
paint.setAntiAlias(true);
// 设置空心圆
paint.setStyle(Style.STROKE);
// 透明
paint.setAlpha(100);
canvas.drawCircle(100, 100, 20, paint);

3)画图片

canvas.drawBitmap(mBitmap, 0, 0, mPaint);

4)画三角形

//设置一个路径 画三角形
Path path = new Path();
int x1=10,y1=20;
int x2=100,y2=100;
int x3=180,y3=20;
//起点
path.moveTo(x1, y1);
//连接点
path.lineTo(x2, y2);
path.lineTo(x3, y3);
path.lineTo(x1, y1);
canvas.drawPath(path, paint);

5)画扇形

// 画扇形 第一个参数:指定一个矩形区域用来展示扇形,第2 和3 参数为角度 第四个参数:true表示显示扇形两边
// false表示隐藏扇形的两边
RectF oval = new RectF(10, 10, 170, 170);
canvas.drawArc(oval, 0, 360, true, paint);


canvas.drawArc(mrectF, 0, -90,true, mPaint);

 11-15 07:00:38.140: E/AndroidRuntime(4346): android.view.ViewRootImpl$CalledFromWrongThreadException: 
 Only the original thread that created a view hierarchy can touch its views. 只有主线程才可以更新ui

 调用postInvalidate();

6)动态画圆

在mainActivity 找到自定义控件,然后给控件添加方法调用
//在主线程更新UI调用此方法
invalidate();
//在子线程更新UI调用此方法
postInvalidate();

/** 动态画圆 **/
public void startCircle(final int i) {
    new Thread() {
        public void run() {
            for (int j = 0; j <= i; j++) {
                SystemClock.sleep(50);
                circle = j;
            }
            // 在子线程更新UI调用此方法
            postInvalidate();
        };
    }.start();

}

// 画扇形 第一个参数:指定一个矩形区域用来展示扇形,第2 和3 参数为角度 第四个参数:true表示显示扇形两边
// false表示隐藏扇形的两边
RectF oval = new RectF(10, 10, 200, 200);

float sweepAngle = circle / 100f * 360;
canvas.drawArc(oval, 0, sweepAngle, false, paint);

7.开关

需求分析:Android中 原生的开关非常丑陋 ,一般在企业中很少使用,需要我们自己定义view

实现代码

1)在布局中声明我们定义的myView

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

<com.ithiema.toogleview.ToogleView
    android:id="@+id/toogleView1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true" />

</RelativeLayout>

2)在myView构造方法中获取开关背景图片(bitmap) 和 滑动块的背景图片

//[1]先获取开关的背景图片和滑动块的图片(bitmap) 
    mtoogleViewbg = BitmapFactory.decodeResource(getResources(), R.drawable.toogle_background);
    mSlideBg = BitmapFactory.decodeResource(getResources(), R.drawable.toogle_slidebg);
    //[2]算出滑块left最大边界值 
    slideLeftMaxSize = mtoogleViewbg.getWidth() - mSlideBg.getWidth();

3)在移动的方法里面算出移动的距离

float moveX = event.getX();
float distanceX = moveX - downX;
slideLeftPosition+=distanceX;
//[3]对边界进行判断
if (slideLeftPosition <=0) {
    slideLeftPosition = 0;
}else if (slideLeftPosition >=slideLeftMaxSize) {
    slideLeftPosition = slideLeftMaxSize;
}
downX = moveX;

4)手指抬起的时候出来的逻辑

//当手指抬起时候 滑动块是往左移动还是往右移动  
//算出滑动块的中心点位置 = slideLeftPosition + 滑动块宽度的一半   < 背景宽度的一半 就往左移动 否则往右移动 
if (slideLeftPosition + mSlideBg.getWidth() /2 < mtoogleViewbg.getWidth()/2 ) {
    slideLeftPosition = 0;
}else {
    slideLeftPosition = slideLeftMaxSize;
}

5)当手指抬起后 实现开关的功能

if (isHandup) {
    isHandup = false;
    //[4]获取开关当前的状态
    boolean isOpenTemp = slideLeftPosition > 0;
    if(isOpen != isOpenTemp && mToogleViewListener!=null){
        //[5]条件满足 触发回调方法
        mToogleViewListener.toogleState(isOpenTemp);
        isOpen = isOpenTemp;
    }
}

6)设置开关的状态

public void setToogleViewState(boolean b) {
    isHandup = true;
    if (b) {
        slideLeftPosition = slideLeftMaxSize; 
    }else {
        slideLeftPosition = 0; 
    }
}

代码:

private ToogleListener toogle;

public MyView(Context context, AttributeSet attrs) {
    super(context, attrs);

    // [1]先获取开关的背景图片和滑动块的图片(bitmap)
    toogle_background = BitmapFactory.decodeResource(getResources(),
            R.drawable.toogle_background);
    toogle_slidebg = BitmapFactory.decodeResource(getResources(),
            R.drawable.toogle_slidebg);
    tooglewidth = toogle_background.getWidth() / 2;
    maxX = toogle_background.getWidth() - toogle_slidebg.getWidth();
    halfX = maxX / 2;

    // 获取自定义的属性
    String name = "http://schemas.android.com/apk/res/com.heima.switchd";
    boolean attributeBoolean = attrs.getAttributeBooleanValue(name,
            "toogleState", false);
    setToogleViewState(attributeBoolean);

}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 这个view大小和背景图片宽高一样
    setMeasuredDimension(toogle_background.getWidth(),
            toogle_background.getHeight());
}

@Override
protected void onDraw(Canvas canvas) {

    canvas.drawBitmap(toogle_background, 0, 0, null);
    canvas.drawBitmap(toogle_slidebg, distend, 0, null);
    // 判断是抬起状态
    if (isUp) {
        isUp = false;
        // 判断当前的位置
        boolean isTemp = distend > 0;
        if (isOpen != isTemp && toogle != null) {
            // [5]条件满足 触发回调方法
            toogle.toogleState(isTemp);
            isOpen = isTemp;
        }
    }
}

// 处理消息
@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
        downX = event.getX();
        // 记录开始的时间
        startTime = System.currentTimeMillis();
        break;
    case MotionEvent.ACTION_MOVE:
        float moveX = event.getX();
        // 移动的距离
        distend += moveX - downX;
        if (distend <= 0) {
            distend = 0;
        } else if (distend >= maxX) {
            distend = maxX;
        }

        // 移动后再次赋值给初始值
        downX = moveX;

        break;
    case MotionEvent.ACTION_UP:
        isUp = true;
        // 记录抬起的时间
        long upTime = System.currentTimeMillis();
        float upX = event.getX();
        distend += upX - downX;
        long time = upTime - startTime;

        if (upX - downX < 5 && time < 200) {
            // 时间小于200 是点击事件
            if (upX > tooglewidth) {
                distend = maxX;
            } else {
                distend = 0;
            }
        } else {
            // 自动弹回
            if (distend <= halfX) {
                distend = 0;
            } else if (distend > halfX) {
                distend = maxX;
            }
        }

        break;

    default:
        break;
    }
    invalidate();// --->ondraw方法会执行
    return true;
}

/** 监听 **/
public void setOnToogleListener(ToogleListener toogleListener) {
    this.toogle = toogleListener;
}

/** 模拟写个接口 包括未实现的方法 **/
public interface ToogleListener {
    void toogleState(boolean result);
}

// 设置开关的状态
public void setToogleViewState(boolean b) {
    isUp = true;
    if (b) {
        distend = maxX;
        System.out.println("111");
    } else {
        distend = 0;
        System.out.println(2);
    }
}

7)自定义开关属性 首先在values下创建attrs文件

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ToogleView">
         <attr name="toogleState" format="boolean" />
    </declare-styleable>

</resources>


接下来自定义命名空间

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:itheima="http://schemas.android.com/apk/res/com.ithiema.toogleview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" >

   <com.ithiema.toogleview.ToogleView
        android:id="@+id/toogleView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        itheima:toogleState="true"
        android:layout_centerInParent="true" />

</RelativeLayout>

在自定义view 的构造方法中初始化

//[3]获取布局中定义的属性值 
String namespace = "http://schemas.android.com/apk/res/com.ithiema.toogleview";
boolean toogleState = attrs.getAttributeBooleanValue(namespace, "toogleState", false);

//[4]设置开关的状态
setToogleViewState(toogleState);

8 今天总结

[1]回顾Android原生控件  了解

[2]下拉列表    练习一下 

[3]view的绘制流程 ---测量---排版---绘制  掌握

[4]绘制的实战  掌握 

[5]开关 掌握

popupwindow 和 listView 组合 下拉选择框

View的绘制流程
1.测量 2.排版(布局) 3.绘制

measure layout draw

作者:yin13753884368 发表于2016/11/18 20:10:06 原文链接
阅读:26 评论:0 查看评论

自定义控件03

$
0
0

#

scrollView

viewpager

fragment

handler 快速搜索

开源项目 slidingmenuhm

左滑 右滑 interceptTouchEvent拦截事件


☆☆☆☆☆去标题栏

// 在onCreate中 去掉标题栏 调用方法
// requestWindowFeature(Window.FEATURE_NO_TITLE);

去掉标题栏
全屏时需要同时加下面的两个item
<style name="AppBaseTheme" parent="android:Theme.Holo.Light.DarkActionBar">
        <item name="android:windowFullscreen">true</item>
        <item name="android:windowNoTitle">true</item>
        <!-- API 14 theme customizations can go here. -->
</style>


iv.setBackgroundResource(picture[i]);//填充背景窗口
iv.setImageResource(picture[i]);//显示的是图片实际的大小

☆☆☆☆☆ViewGroup

继承ViewGroup 需要重新测量,看布局宽和高的值
/*@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //如果布局的宽和高是固定的值 设置measureChildren(0,0);否则 measureChildren(widthMeasureSpec, heightMeasureSpec);
    measureChildren(widthMeasureSpec, heightMeasureSpec);
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}*/

☆☆☆☆☆ViewGroup

自定义的类继承了RelativeLayout 不需要再次测量onmeasure();和排版onLayout();

ViewPager使用 实现图片无限循环播放

    public class MainActivity extends Activity {
    private int[] picture = new int[] { R.drawable.icon_1, R.drawable.icon_2,
            R.drawable.icon_3, R.drawable.icon_4, R.drawable.icon_5 };
    private String[] descs = { "为梦想坚持", "我相信我是黑马", "黑马公开课", "Google/IO",
            "轻松1w+" };
    private ArrayList<ImageView> list;// ImageView 图片
    private ArrayList<View> dots;// View 小黑点
    private ViewPager pager;
    private TextView tv_content;
    private LinearLayout ll_layout;
    private View currentView;
    /** 用handler实现自动滚动 **/
    private Handler handler = new Handler() {
        public void handleMessage(android.os.Message msg) {
            // 显示下一个图片
            pager.setCurrentItem(pager.getCurrentItem() + 1);
            // 再次发送,实现无限滚动
            handler.sendEmptyMessageDelayed(10, 3500);
        };
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 去掉标题栏
        // requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_main);
        /** 初始化数据 **/
        initData();
        /** 初始化adapter **/
        initAdapter();
        /** 设置pager滚动监听的事件 **/
        pager.setOnPageChangeListener(new Listen());

    }

    /** 屏幕可见时 实现自动滚动图片 **/
    @Override
    protected void onStart() {
        // 参数1:是标记 参数2:间隔时间
        handler.sendEmptyMessageDelayed(10, 3500);
        super.onStart();
    }

    /** 屏幕不可见时 关闭自动滚动图片 **/
    @Override
    protected void onStop() {
        // 10 用于识别handler,作为标记
        handler.removeMessages(10);
        super.onStop();
    }

    /** 设置pager滚动监听的事件 **/
    public class Listen implements OnPageChangeListener {
        // 滚动
        @Override
        public void onPageScrolled(int position, float positionOffset,
                int positionOffsetPixels) {

        }

        // 当一个新的页面被选中的时候调用
        @Override
        public void onPageSelected(int position) {
            // 小圆点和内容的改变 封装成方法,方便调用
            dot_contentChange(position);

        }

        // 当滑动页面的状态改变的时候调用
        @Override
        public void onPageScrollStateChanged(int state) {

        }

    }

    /** 小圆点和内容的改变 封装成方法,方便调用 **/
    private void dot_contentChange(int position) {
        // [1]动态改变小圆点的文本内容 数据在哪里存着就去哪里取
        tv_content.setText(descs[position % 5]);
        View dot = dots.get(position % 5);
        if (currentView != null) {
            currentView.setSelected(false);
        }
        // [2]更新小圆点的状态 小圆点在集合里面存着
        dot.setSelected(true);
        // [3]把dots.get(position % 5)赋值给currentView
        currentView = dot;
    }

    /** 初始化adapter **/
    private void initAdapter() {
        pager.setAdapter(new MyPagerAdapter());
        // 加载设置 实现既可以往左滑又可以往右滑
        pager.setCurrentItem(1000000000 / 2);
    }

    /** 初始化数据 **/
    private void initData() {
        // viewpager控件
        pager = (ViewPager) findViewById(R.id.viewpager1);
        // 显示的文本内容
        tv_content = (TextView) findViewById(R.id.tv_content);

        ll_layout = (LinearLayout) findViewById(R.id.ll_layout);
        // 显示小黑点
        list = new ArrayList<ImageView>();
        dots = new ArrayList<View>();
        for (int i = 0; i < picture.length; i++) {
            ImageView iv = new ImageView(getApplicationContext());
            iv.setBackgroundResource(picture[i]);
            // 显示的是图片实际的大小
            // iv.setImageResource(picture[i]);
            list.add(iv);
        }
        /** 初始化小黑点 自己绘制加参数 **/
        for (int i = 0; i < picture.length; i++) {
            // [1]初始化小圆点的view
            View view = new View(getApplicationContext());
            // [2]设置dotView宽和高
            LayoutParams params = new LayoutParams(7, 7);
            params.bottomMargin = 10;
            if (i != 0) {
                params.leftMargin = 7;
            }
            view.setLayoutParams(params);
            view.setBackgroundResource(R.drawable.dot_selector);
            ll_layout.addView(view);
            dots.add(view);
        }
        // 初始化数据时将第一张的文字和小圆点选中
        dot_contentChange(0);
    }

    /** 初始化adapter **/
    public class MyPagerAdapter extends PagerAdapter {
        /** 返回一个极大值实现无限循环 **/
        @Override
        public int getCount() {

            // return list.size();
            return 1000000000;
            // return Integer.MAX_VALUE;

        }

        /**
         * Object:就是在instantiateItem方法里面返回的内容
         */
        @Override
        public boolean isViewFromObject(View view, Object object) {
            return view == object;
        }

        /**
         * 初始化条目的内容 类似lsitview getView方法 container:就是viewPager
         * position:每个页面的对应的位置
         */

        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            ImageView imageView = list.get(position % 5);

            container.addView(imageView);
            return imageView;
        }

        /**
         * 移除不用的页面 container:viewPager object:就是instantiateItem方法里面的返回值
         */

        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {

            container.removeView((View) object);
        }

    }


侧滑菜单

  功能分析:实际上是两个页面 通过一个viewGroup进行包裹 

  menu菜单实际上就是一个scrollView包裹了一个垂直的线性布局,scrollView只能包裹一个孩子.

[1]搭建页面 大家主页面 和 菜单页面

   主页面布局

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" >

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@drawable/top_bar_bg"
            android:orientation="horizontal" >

            <Button
                android:id="@+id/btn_back"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/main_back" />

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingTop="3dp"
                android:src="@drawable/top_bar_divider" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginLeft="50dp"
                android:text="黑马新闻"
                android:textColor="#ffffff"
                android:textSize="25sp" />
        </LinearLayout>

          <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:text="钓鱼岛是中国的,\nXXX是世界的"
                android:gravity="center"
                android:textSize="25sp" />

    </LinearLayout>


     菜单页面布局

    <?xml version="1.0" encoding="utf-8"?>
    <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="240dp"
        android:layout_height="match_parent" >

        <LinearLayout
            android:layout_width="240dp"
            android:layout_height="match_parent"
            android:background="@drawable/menu_bg"
            android:orientation="vertical" >

            <TextView
                style="@style/MenuText"
                android:background="#571F2C"
                android:drawableLeft="@drawable/tab_news"
                android:text="新闻" />

            <TextView
                style="@style/MenuText"
                android:drawableLeft="@drawable/tab_read"
                android:text="订阅" />

            <TextView
                style="@style/MenuText"
                android:drawableLeft="@drawable/tab_ties"
                android:text="跟帖" />

            <TextView
                style="@style/MenuText"
                android:drawableLeft="@drawable/tab_pics"
                android:text="图片" />

            <TextView
                style="@style/MenuText"
                android:drawableLeft="@drawable/tab_ugc"
                android:text="话题" />

            <TextView
                style="@style/MenuText"
                android:drawableLeft="@drawable/tab_vote"
                android:text="投票" />

            <TextView
                style="@style/MenuText"
                android:drawableLeft="@drawable/tab_focus"
                android:text="聚合阅读" />
        </LinearLayout>

    </ScrollView>

[2]定义一个viewGroup (slidingMenu) 把我们刚刚定义的布局加到slidingMenu里面

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

        <com.itheima.slidingmenuhm.SlidingMenu
            android:id="@+id/slidingMenu1"
            android:layout_width="match_parent"
            android:layout_height="match_parent" >

            <!-- 添加自己的孩子  menu菜单和main主界面 -->

            <include layout="@layout/menu" />
            <include layout="@layout/main" />

        </com.itheima.slidingmenuhm.SlidingMenu>

    </RelativeLayout>

[3]由于我定义的slidingmenu继承自相对布局 系统默认实现了测量和排版 所以不需要我们在进行测量 但是我们需要排版,因为系统的排版方式不是我们想要的 所以我们自己重写onLayout方法对孩子进行排版

    //在这个方法里面 自己对孩子进行排版 不使用系统默认的排版方式
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {

            //[1]找到menu和main孩子 
            View menuView = getChildAt(0);
            View mainView = getChildAt(1);
            //[2]获取菜单的宽度 
            menuWidth = menuView.getMeasuredWidth();
            //[3]对menu菜单进行排版
            menuView.layout(-menuWidth, t, l, b);
            //[4]对main界面排版
            mainView.layout(l, t, r, b);

        }

[4]当用户手指滑动的时候 算出移动的距离 让菜单滑出来 需要我们重写OnTouchEvent方法处理事件

    case MotionEvent.ACTION_DOWN:  //按下
                //[1]获取手指按下的坐标
                downX = event.getX();
                break;

            case MotionEvent.ACTION_MOVE: //移动
                //[2]算出移动的距离 
                float moveX = event.getX();
                distaceX = (int) (moveX - downX)+currentMenuPosition;
                //[3]对边界进行处理
                if (distaceX <= 0) {
                    distaceX = 0;
                }else if (distaceX >=menuWidth) {
                    distaceX = menuWidth;
                }

                //[4]开始滚动view
                startScrollViewContent(distaceX);
                break;

[5]我们自己定义了一个view滚动的方法 让view 的内容进行滚动 因为系统的ScrollTo方法不好用

    /**
         * 由于scrollTo 方法 系统在实现的时候 传入正值往左移动  传入负值往右移动 所以我重写这个方 
         *  符合中国 人的思维
         * @param x
         */
        public void startScrollViewContent(int x){
            super.scrollTo(-x, 0);
        }

[6]处理手指抬起的逻辑 ,当移动的距离<菜单的宽/2 就让菜单回到0 否则把菜单全部显示

    case MotionEvent.ACTION_UP:    //抬起
                //[5]当手指抬起后, 如果移动的距离 < 菜单的宽/2 就回到左边  否则像右移动
                if (distaceX < menuWidth/2) {
                    currentMenuPosition = 0;
                }else {
                    currentMenuPosition = menuWidth;
                }

                //[6]当一段逻辑同时用到多次 我们最好做抽取

                int startX = distaceX; //就是移动的距离
                int endX = currentMenuPosition;
                //[7]实现平滑滚动
                startScroller(startX, endX);

                break;

[7]当menu菜单打开后,我在菜单上向左滑动 发现不能滑动,因为孩子消费了事件,所以我们在slidingmenu(viewGroup)里的onInterceptTouchevent里面处理事件(拦截事件).

        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                 downX = ev.getX();
                 downY = ev.getY();

                break;

            case MotionEvent.ACTION_MOVE:
                float moveX = ev.getX();
                float moveY = ev.getY();
                //[1]算出X轴移动的距离 和 Y移动的距离 
                int distanceX = (int) (moveX - downX);
                int distanceY = (int) (moveY - downY); 
                //[2]如果x轴移动的距离大于Y轴移动的距离 就拦截事件
                if (Math.abs(distanceX) > Math.abs(distanceY)) {
                    return true; 
                }
                break;

            case MotionEvent.ACTION_UP:
                break;
            }

            return super.onInterceptTouchEvent(ev);
        }

[8]点击主页面的按钮判断菜单是否打开或者关闭

    //通过这个方法控制菜单打开或者关闭
        public void setOnMenuIsOpen() {
            int startX = 0;
            if (currentMenuPosition == 0) {
                //说明菜单是关闭状态  需要打开 
                currentMenuPosition = menuWidth;
            }else if (currentMenuPosition == menuWidth) {
                //说明菜单是打开状态  需要关闭
                currentMenuPosition = 0;
                startX = menuWidth;

            }
            //调用平滑滚动的方法
            startScroller(startX, currentMenuPosition);


        }   

2 广告条效果

       使用viewpager实现 直接放到了v4包里,fragment 

       viewpager直接继承ViewGroup 需要往viewpager里面添加孩子.

       viewpager可以让用户左右滑动 和listview相反

       viewpager展示数据的原理和listview一样,   使用自己的适配器展示数据(pagerAdapter)

       使用的步骤

[1]在布局里面声明viewpager

    <android.support.v4.view.ViewPager
            android:id="@+id/vp"
            android:layout_width="match_parent"
            android:layout_height="180dp" >
    </android.support.v4.view.ViewPager>

[2]关联 v4包的源码

     2.1)在libs下创建一个android-support-v4.jar.properties配置文件 

     2.2)配置文件里面的代码如下

     src=D:\\Android\\adt-bundle-windows-x86_64_20140101\\sdk\\extras\\android\\support\\v4\\src

     2.3)关闭工程在打开工程即可

[3]创建viewpager的适配器

    //创建viewpager需要的适配器
        class MyPagerAdapter extends PagerAdapter{

            //展示viewpager条目的数量
            @Override
            public int getCount() {
                return ivs.size();
            }

            /**
             * Object:就是在instantiateItem方法里面返回的内容
             */
            @Override
            public boolean isViewFromObject(View view, Object object) {
                return view == object;
            }

            /**
             * 初始化条目的内容  类似lsitview getView方法
             * container:就是viewPager
             * position:每个页面的对应的位置
             */
            @Override
            public Object instantiateItem(ViewGroup container, int position) {
                //[1]根据位置 去集合里面取数据 
                ImageView iv = ivs.get(position);
                //[2]获取到每个iv后加入到viewpager中
                container.addView(iv);
                return iv;
            }

            /**
             * 移除不用到页面
             * container:viewPager 
             * object:就是instantiateItem方法里面的返回值
             */
            @Override
            public void destroyItem(ViewGroup container, int position, Object object) {
                container.removeView((View) object);
            }


        }

[4]添加小圆点的布局

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignBottom="@id/vp"
            android:background="#66000000"
            android:gravity="center_horizontal"
            android:orientation="vertical"
            android:paddingBottom="5dp"
            android:paddingTop="5dp" >

            <TextView
                android:id="@+id/tv_content"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="我是小圆点对应的内容"
                android:textColor="#ffffff"
                android:textSize="17sp" />
            <!-- 动态的往布局里面添加小圆点 -->

            <LinearLayout
                android:id="@+id/ll_layout"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="4dp"
                android:orientation="horizontal" >

                <!-- <View
                    android:layout_width="5dp"
                    android:layout_height="5dp"
                    android:background="@drawable/dot_selector" /> -->
            </LinearLayout>
        </LinearLayout>

[5]给viewpager设置页面滚动的监听

    //代表viewpager的监听
        OnPageChangeListener listener = new OnPageChangeListener() {

            //当一个新的页面被选中的时候调用
            @Override
            public void onPageSelected(int position) {

                changeDotstateAndText(position);

            }

            //当页面开始滚动
            @Override
            public void onPageScrolled(int position, float positionOffset,
                    int positionOffsetPixels) {

            }
            //当滑动页面的状态改变的时候调用
            @Override
            public void onPageScrollStateChanged(int state) {

            }
        };

[6]动态改变小圆点状态和对应文本的内容 由于2个地方都需要改变小圆点的状态和对应的内容 所以我们抽出一个方法

    /**改变小圆点的状态和对应文本的内容**/
        private void changeDotstateAndText(int position) {
            //[1]动态改变小圆点的文本内容  数据在哪里存着就去哪里取
            tv_content.setText(descs[position]);

            if (currentDotView!=null) {
                currentDotView.setSelected(false);
            }
            //[2]更新小圆点的状态  小圆点在集合里面存着 
            dotLists.get(position).setSelected(true);

            //[3]把dotLists.get(position)赋值给currentDotView
            currentDotView = dotLists.get(position);
        }

[7]实现viewpager的无限循环

        0 % 5 = 0

        1 % 5 = 1

        2 %5 =2

        3 %5=3;

        4%5=4

        5%5 = 0 

        6%5 = 1

        7%5=2 .......

        就是在适配器的getcount方法里面返回一个比较大的数就可以了 把position对5取%

[8]实现自动切换

      在onStart方法里面使用handler发一个消息 

    //当界面可见的时候执行 
        @Override
        protected void onStart() {
            //4秒钟后 显示下一个页面的内容
            handler.sendEmptyMessageDelayed(10, 4000);
            super.onStart();
        }


      在handlermessage方法里面处理消息

    private Handler handler = new Handler(){
            //处理消息
            public void handleMessage(android.os.Message msg) {
                //显示viewpager下个页面的内容 
                vp.setCurrentItem(vp.getCurrentItem()+1);
                handler.sendEmptyMessageDelayed(10, 4000);
            };
        };

[9]当viewpager初始化的时候既可以往左滑动,又可以往又滑动

    1.vp.setCurrentItem(vpMaxSize/2);                   

作者:yin13753884368 发表于2016/11/18 20:12:00 原文链接
阅读:35 评论:0 查看评论

Android开发之XML文件的解析的三种方法

$
0
0

Android开发之XML文件的解析的三种方法

486人阅读 评论(1) 收藏 举报

XML在各种开发中都广泛应用,Android也不例外。作为承载数据的一个重要角色,如何读写XML成为Android开发中一项重要的技能。今天就由我向大家介绍一下在Android平台下几种常见的XML解析和创建的方法。

在Android中,常见的XML解析器分别为SAX解析器、DOM解析器和PULL解析器,下面,我将一一向大家详细介绍。

SAX解析器:

SAX(Simple API for XML)解析器是一种基于事件的解析器,它的核心是事件处理模式,主要是围绕着事件源以及事件处理器来工作的。当事件源产生事件后,调用事件处理器相应的处理方法,一个事件就可以得到处理。在事件源调用事件处理器中特定方法的时候,还要传递给事件处理器相应事件的状态信息,这样事件处理器才能够根据提供的事件信息来决定自己的行为。

SAX解析器的优点是解析速度快,占用内存少。非常适合在Android移动设备中使用。

DOM解析器:

DOM是基于树形结构的的节点或信息片段的集合,允许开发人员使用DOM API遍历XML树、检索所需数据。分析该结构通常需要加载整个文档和构造树形结构,然后才可以检索和更新节点信息。

由于DOM在内存中以树形结构存放,因此检索和更新效率会更高。但是对于特别大的文档,解析和加载整个文档将会很耗资源。

PULL解析器:

PULL解析器的运行方式和SAX类似,都是基于事件的模式。不同的是,在PULL解析过程中,我们需要自己获取产生的事件然后做相应的操作,而不像SAX那样由处理器触发一种事件的方法,执行我们的代码。PULL解析器小巧轻便,解析速度快,简单易用,非常适合在Android移动设备中使用,Android系统内部在解析各种XML时也是用PULL解析器。

以上三种解析器,都是非常实用的解析器,我将会一一介绍。我们将会使用这三种解析技术完成一项共同的任务。

我们新建一个项目,项目结构如下:

我会在项目的assets目录中放置一个XML文档books.xml,内容如下:

  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <books>  
  3.     <book>  
  4.         <id>1001</id>  
  5.         <name>Thinking In Java</name>  
  6.         <price>80.00</price>  
  7.     </book>  
  8.     <book>  
  9.         <id>1002</id>  
  10.         <name>Core Java</name>  
  11.         <price>90.00</price>  
  12.     </book>  
  13.     <book>  
  14.         <id>1003</id>  
  15.         <name>Hello, Andriod</name>  
  16.         <price>100.00</price>  
  17.     </book>  
  18. </books>  

然后我们分别使用以上三种解析技术解析文档,得到一个List<Book>的对象,先来看一下Book.Java的代码:

  1. package com.scott.xml.model;  
  2.   
  3. public class Book {  
  4.     private int id;  
  5.     private String name;  
  6.     private float price;  
  7.       
  8.     public int getId() {  
  9.         return id;  
  10.     }  
  11.   
  12.     public void setId(int id) {  
  13.         this.id = id;  
  14.     }  
  15.   
  16.     public String getName() {  
  17.         return name;  
  18.     }  
  19.   
  20.     public void setName(String name) {  
  21.         this.name = name;  
  22.     }  
  23.   
  24.     public float getPrice() {  
  25.         return price;  
  26.     }  
  27.   
  28.     public void setPrice(float price) {  
  29.         this.price = price;  
  30.     }  
  31.   
  32.     @Override  
  33.     public String toString() {  
  34.         return "id:" + id + ", name:" + name + ", price:" + price;  
  35.     }  
  36. }  

最后,我们还要把这个集合对象中的数据生成一个新的XML文档,如图:

生成的XML结构跟原始文档略有不同,是下面这种格式:

  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <books>  
  3.   <book id="1001">  
  4.     <name>Thinking In Java</name>  
  5.     <price>80.0</price>  
  6.   </book>  
  7.   <book id="1002">  
  8.     <name>Core Java</name>  
  9.     <price>90.0</price>  
  10.   </book>  
  11.   <book id="1003">  
  12.     <name>Hello, Andriod</name>  
  13.     <price>100.0</price>  
  14.   </book>  
  15. </books>  

接下来,就该介绍操作过程了,我们先为解析器定义一个BookParser接口,每种类型的解析器需要实现此接口。BookParser.java代码如下:

  1. package com.scott.xml.parser;  
  2.   
  3. import java.io.InputStream;  
  4. import java.util.List;  
  5.   
  6. import com.scott.xml.model.Book;  
  7.   
  8. public interface BookParser {  
  9.     /** 
  10.      * 解析输入流 得到Book对象集合 
  11.      * @param is 
  12.      * @return 
  13.      * @throws Exception 
  14.      */  
  15.     public List<Book> parse(InputStream is) throws Exception;  
  16.       
  17.     /** 
  18.      * 序列化Book对象集合 得到XML形式的字符串 
  19.      * @param books 
  20.      * @return 
  21.      * @throws Exception 
  22.      */  
  23.     public String serialize(List<Book> books) throws Exception;  
  24. }  

好了,我们就该一个一个的实现该接口,完成我们的解析过程。

使用SAX解析器:

SaxBookParser.java代码如下:

  1. package com.scott.xml.parser;  
  2.   
  3. import java.io.InputStream;  
  4. import java.io.StringWriter;  
  5. import java.util.ArrayList;  
  6. import java.util.List;  
  7.   
  8. import javax.xml.parsers.SAXParser;  
  9. import javax.xml.parsers.SAXParserFactory;  
  10. import javax.xml.transform.OutputKeys;  
  11. import javax.xml.transform.Result;  
  12. import javax.xml.transform.Transformer;  
  13. import javax.xml.transform.TransformerFactory;  
  14. import javax.xml.transform.sax.SAXTransformerFactory;  
  15. import javax.xml.transform.sax.TransformerHandler;  
  16. import javax.xml.transform.stream.StreamResult;  
  17.   
  18. import org.xml.sax.Attributes;  
  19. import org.xml.sax.SAXException;  
  20. import org.xml.sax.helpers.AttributesImpl;  
  21. import org.xml.sax.helpers.DefaultHandler;  
  22.   
  23. import com.scott.xml.model.Book;  
  24.   
  25. public class SaxBookParser implements BookParser {  
  26.       
  27.     @Override  
  28.     public List<Book> parse(InputStream is) throws Exception {  
  29.         SAXParserFactory factory = SAXParserFactory.newInstance();  //取得SAXParserFactory实例  
  30.         SAXParser parser = factory.newSAXParser();                  //从factory获取SAXParser实例  
  31.         MyHandler handler = new MyHandler();                        //实例化自定义Handler  
  32.         parser.parse(is, handler);                                  //根据自定义Handler规则解析输入流  
  33.         return handler.getBooks();  
  34.     }  
  35.   
  36.     @Override  
  37.     public String serialize(List<Book> books) throws Exception {  
  38.         SAXTransformerFactory factory = (SAXTransformerFactory) TransformerFactory.newInstance();//取得SAXTransformerFactory实例  
  39.         TransformerHandler handler = factory.newTransformerHandler();           //从factory获取TransformerHandler实例  
  40.         Transformer transformer = handler.getTransformer();                     //从handler获取Transformer实例  
  41.         transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");            // 设置输出采用的编码方式  
  42.         transformer.setOutputProperty(OutputKeys.INDENT, "yes");                // 是否自动添加额外的空白  
  43.         transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");   // 是否忽略XML声明  
  44.           
  45.         StringWriter writer = new StringWriter();  
  46.         Result result = new StreamResult(writer);  
  47.         handler.setResult(result);  
  48.           
  49.         String uri = "";    //代表命名空间的URI 当URI无值时 须置为空字符串  
  50.         String localName = "";  //命名空间的本地名称(不包含前缀) 当没有进行命名空间处理时 须置为空字符串  
  51.           
  52.         handler.startDocument();  
  53.         handler.startElement(uri, localName, "books"null);  
  54.           
  55.         AttributesImpl attrs = new AttributesImpl();    //负责存放元素的属性信息  
  56.         char[] ch = null;  
  57.         for (Book book : books) {  
  58.             attrs.clear();  //清空属性列表  
  59.             attrs.addAttribute(uri, localName, "id""string", String.valueOf(book.getId()));//添加一个名为id的属性(type影响不大,这里设为string)  
  60.             handler.startElement(uri, localName, "book", attrs);    //开始一个book元素 关联上面设定的id属性  
  61.               
  62.             handler.startElement(uri, localName, "name"null); //开始一个name元素 没有属性  
  63.             ch = String.valueOf(book.getName()).toCharArray();  
  64.             handler.characters(ch, 0, ch.length);   //设置name元素的文本节点  
  65.             handler.endElement(uri, localName, "name");  
  66.               
  67.             handler.startElement(uri, localName, "price"null);//开始一个price元素 没有属性  
  68.             ch = String.valueOf(book.getPrice()).toCharArray();  
  69.             handler.characters(ch, 0, ch.length);   //设置price元素的文本节点  
  70.             handler.endElement(uri, localName, "price");  
  71.               
  72.             handler.endElement(uri, localName, "book");  
  73.         }  
  74.         handler.endElement(uri, localName, "books");  
  75.         handler.endDocument();  
  76.           
  77.         return writer.toString();  
  78.     }  
  79.       
  80.     //需要重写DefaultHandler的方法  
  81.     private class MyHandler extends DefaultHandler {  
  82.   
  83.         private List<Book> books;  
  84.         private Book book;  
  85.         private StringBuilder builder;  
  86.           
  87.         //返回解析后得到的Book对象集合  
  88.         public List<Book> getBooks() {  
  89.             return books;  
  90.         }  
  91.           
  92.         @Override  
  93.         public void startDocument() throws SAXException {  
  94.             super.startDocument();  
  95.             books = new ArrayList<Book>();  
  96.             builder = new StringBuilder();  
  97.         }  
  98.   
  99.         @Override  
  100.         public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {  
  101.             super.startElement(uri, localName, qName, attributes);  
  102.             if (localName.equals("book")) {  
  103.                 book = new Book();  
  104.             }  
  105.             builder.setLength(0);   //将字符长度设置为0 以便重新开始读取元素内的字符节点  
  106.         }  
  107.           
  108.         @Override  
  109.         public void characters(char[] ch, int start, int length) throws SAXException {  
  110.             super.characters(ch, start, length);  
  111.             builder.append(ch, start, length);  //将读取的字符数组追加到builder中  
  112.         }  
  113.           
  114.         @Override  
  115.         public void endElement(String uri, String localName, String qName) throws SAXException {  
  116.             super.endElement(uri, localName, qName);  
  117.             if (localName.equals("id")) {  
  118.                 book.setId(Integer.parseInt(builder.toString()));  
  119.             } else if (localName.equals("name")) {  
  120.                 book.setName(builder.toString());  
  121.             } else if (localName.equals("price")) {  
  122.                 book.setPrice(Float.parseFloat(builder.toString()));  
  123.             } else if (localName.equals("book")) {  
  124.                 books.add(book);  
  125.             }  
  126.         }  
  127.     }  
  128. }  

代码中,我们定义了自己的事件处理逻辑,重写了DefaultHandler的几个重要的事件方法。下面我为大家着重介绍一下DefaultHandler的相关知识。DefaultHandler是一个事件处理器,可以接收解析器报告的所有事件,处理所发现的数据。它实现了EntityResolver接口、DTDHandler接口、ErrorHandler接口和ContentHandler接口。这几个接口代表不同类型的事件处理器。我们着重介绍一下ContentHandler接口。结构如图:

这几个比较重要的方法已被我用红线标注,DefaultHandler实现了这些方法,但在方法体内没有做任何事情,因此我们在使用时必须覆写相关的方法。最重要的是startElement方法、characters方法和endElement方法。当执行文档时遇到起始节点,startElement方法将会被调用,我们可以获取起始节点相关信息;然后characters方法被调用,我们可以获取节点内的文本信息;最后endElement方法被调用,我们可以做收尾的相关操作。

最后,我们需要调用SAX解析程序,这个步骤在MainActivity中完成:

  1. package com.scott.xml;  
  2.   
  3. import java.io.FileOutputStream;  
  4. import java.io.InputStream;  
  5. import java.util.List;  
  6.   
  7. import android.app.Activity;  
  8. import android.content.Context;  
  9. import android.os.Bundle;  
  10. import android.util.Log;  
  11. import android.view.View;  
  12. import android.widget.Button;  
  13.   
  14. import com.scott.xml.model.Book;  
  15. import com.scott.xml.parser.BookParser;  
  16. import com.scott.xml.parser.SaxBookParser;  
  17.   
  18. public class MainActivity extends Activity {  
  19.       
  20.     private static final String TAG = "XML";  
  21.       
  22.     private BookParser parser;  
  23.     private List<Book> books;  
  24.       
  25.     @Override  
  26.     public void onCreate(Bundle savedInstanceState) {  
  27.         super.onCreate(savedInstanceState);  
  28.         setContentView(R.layout.main);  
  29.           
  30.         Button readBtn = (Button) findViewById(R.id.readBtn);  
  31.         Button writeBtn = (Button) findViewById(R.id.writeBtn);  
  32.           
  33.         readBtn.setOnClickListener(new View.OnClickListener() {  
  34.             @Override  
  35.             public void onClick(View v) {  
  36.                 try {  
  37.                     InputStream is = getAssets().open("books.xml");  
  38.                     parser = new SaxBookParser();  //创建SaxBookParser实例  
  39.                     books = parser.parse(is);  //解析输入流  
  40.                     for (Book book : books) {  
  41.                         Log.i(TAG, book.toString());  
  42.                     }  
  43.                 } catch (Exception e) {  
  44.                     Log.e(TAG, e.getMessage());  
  45.                 }  
  46.             }  
  47.         });  
  48.         writeBtn.setOnClickListener(new View.OnClickListener() {  
  49.             @Override  
  50.             public void onClick(View v) {  
  51.                 try {  
  52.                     String xml = parser.serialize(books);  //序列化  
  53.                     FileOutputStream fos = openFileOutput("books.xml", Context.MODE_PRIVATE);  
  54.                     fos.write(xml.getBytes("UTF-8"));  
  55.                 } catch (Exception e) {  
  56.                     Log.e(TAG, e.getMessage());  
  57.                 }  
  58.             }  
  59.         });  
  60.     }  
  61. }  

界面就两个按钮,顺便给大家贴上:

 

点击“readXML”按钮,将会调用SAX解析器解析文档,并在日志台打印相关信息:

然后再点击“writeXML”按钮,将会在该应用包下的files目录生成一个books.xml文件:

使用DOM解析器:

DomBookParser.java代码如下:

  1. package com.scott.xml.parser;  
  2.   
  3. import java.io.InputStream;  
  4. import java.io.StringWriter;  
  5. import java.util.ArrayList;  
  6. import java.util.List;  
  7.   
  8. import javax.xml.parsers.DocumentBuilder;  
  9. import javax.xml.parsers.DocumentBuilderFactory;  
  10. import javax.xml.transform.OutputKeys;  
  11. import javax.xml.transform.Result;  
  12. import javax.xml.transform.Source;  
  13. import javax.xml.transform.Transformer;  
  14. import javax.xml.transform.TransformerFactory;  
  15. import javax.xml.transform.dom.DOMSource;  
  16. import javax.xml.transform.stream.StreamResult;  
  17.   
  18. import org.w3c.dom.Document;  
  19. import org.w3c.dom.Element;  
  20. import org.w3c.dom.Node;  
  21. import org.w3c.dom.NodeList;  
  22.   
  23. import com.scott.xml.model.Book;  
  24.   
  25. public class DomBookParser implements BookParser {  
  26.   
  27.     @Override  
  28.     public List<Book> parse(InputStream is) throws Exception {  
  29.         List<Book> books = new ArrayList<Book>();  
  30.         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();  //取得DocumentBuilderFactory实例  
  31.         DocumentBuilder builder = factory.newDocumentBuilder(); //从factory获取DocumentBuilder实例  
  32.         Document doc = builder.parse(is);   //解析输入流 得到Document实例  
  33.         Element rootElement = doc.getDocumentElement();  
  34.         NodeList items = rootElement.getElementsByTagName("book");  
  35.         for (int i = 0; i < items.getLength(); i++) {  
  36.             Book book = new Book();  
  37.             Node item = items.item(i);  
  38.             NodeList properties = item.getChildNodes();  
  39.             for (int j = 0; j < properties.getLength(); j++) {  
  40.                 Node property = properties.item(j);  
  41.                 String nodeName = property.getNodeName();  
  42.                 if (nodeName.equals("id")) {  
  43.                     book.setId(Integer.parseInt(property.getFirstChild().getNodeValue()));  
  44.                 } else if (nodeName.equals("name")) {  
  45.                     book.setName(property.getFirstChild().getNodeValue());  
  46.                 } else if (nodeName.equals("price")) {  
  47.                     book.setPrice(Float.parseFloat(property.getFirstChild().getNodeValue()));  
  48.                 }  
  49.             }  
  50.             books.add(book);  
  51.         }  
  52.         return books;  
  53.     }  
  54.   
  55.     @Override  
  56.     public String serialize(List<Book> books) throws Exception {  
  57.         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();  
  58.         DocumentBuilder builder = factory.newDocumentBuilder();  
  59.         Document doc = builder.newDocument();   //由builder创建新文档  
  60.           
  61.         Element rootElement = doc.createElement("books");  
  62.   
  63.         for (Book book : books) {  
  64.             Element bookElement = doc.createElement("book");  
  65.             bookElement.setAttribute("id", book.getId() + "");  
  66.               
  67.             Element nameElement = doc.createElement("name");  
  68.             nameElement.setTextContent(book.getName());  
  69.             bookElement.appendChild(nameElement);  
  70.               
  71.             Element priceElement = doc.createElement("price");  
  72.             priceElement.setTextContent(book.getPrice() + "");  
  73.             bookElement.appendChild(priceElement);  
  74.               
  75.             rootElement.appendChild(bookElement);  
  76.         }  
  77.           
  78.         doc.appendChild(rootElement);  
  79.           
  80.         TransformerFactory transFactory = TransformerFactory.newInstance();//取得TransformerFactory实例  
  81.         Transformer transformer = transFactory.newTransformer();    //从transFactory获取Transformer实例  
  82.         transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");            // 设置输出采用的编码方式  
  83.         transformer.setOutputProperty(OutputKeys.INDENT, "yes");                // 是否自动添加额外的空白  
  84.         transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");   // 是否忽略XML声明  
  85.           
  86.         StringWriter writer = new StringWriter();  
  87.           
  88.         Source source = new DOMSource(doc); //表明文档来源是doc  
  89.         Result result = new StreamResult(writer);//表明目标结果为writer  
  90.         transformer.transform(source, result);  //开始转换  
  91.           
  92.         return writer.toString();  
  93.     }  
  94.   
  95. }  

然后再MainActivity中只需改一个地方:

 

  1.      readBtn.setOnClickListener(new View.OnClickListener() {  
  2. @Override  
  3. public void onClick(View v) {  
  4.     try {  
  5.         InputStream is = getAssets().open("books.xml");  
  6.  //             parser = new SaxBookParser();  
  7.         parser = new DomBookParser();  
  8.         books = parser.parse(is);  
  9.         for (Book book : books) {  
  10.             Log.i(TAG, book.toString());  
  11.         }  
  12.     } catch (Exception e) {  
  13.         Log.e(TAG, e.getMessage());  
  14.     }  
  15. }  
  16. );  

执行结果是一样的。

使用PULL解析器:

PullBookParser.java代码如下:

  1. package com.scott.xml.parser;  
  2.   
  3. import java.io.InputStream;  
  4. import java.io.StringWriter;  
  5. import java.util.ArrayList;  
  6. import java.util.List;  
  7.   
  8. import org.xmlpull.v1.XmlPullParser;  
  9. import org.xmlpull.v1.XmlSerializer;  
  10.   
  11. import android.util.Xml;  
  12.   
  13. import com.scott.xml.model.Book;  
  14.   
  15. public class PullBookParser implements BookParser {  
  16.       
  17.     @Override  
  18.     public List<Book> parse(InputStream is) throws Exception {  
  19.         List<Book> books = null;  
  20.         Book book = null;  
  21.           
  22. //      XmlPullParserFactory factory = XmlPullParserFactory.newInstance();  
  23. //      XmlPullParser parser = factory.newPullParser();  
  24.           
  25.         XmlPullParser parser = Xml.newPullParser(); //由android.util.Xml创建一个XmlPullParser实例  
  26.         parser.setInput(is, "UTF-8");               //设置输入流 并指明编码方式  
  27.   
  28.         int eventType = parser.getEventType();  
  29.         while (eventType != XmlPullParser.END_DOCUMENT) {  
  30.             switch (eventType) {  
  31.             case XmlPullParser.START_DOCUMENT:  
  32.                 books = new ArrayList<Book>();  
  33.                 break;  
  34.             case XmlPullParser.START_TAG:  
  35.                 if (parser.getName().equals("book")) {  
  36.                     book = new Book();  
  37.                 } else if (parser.getName().equals("id")) {  
  38.                     eventType = parser.next();  
  39.                     book.setId(Integer.parseInt(parser.getText()));  
  40.                 } else if (parser.getName().equals("name")) {  
  41.                     eventType = parser.next();  
  42.                     book.setName(parser.getText());  
  43.                 } else if (parser.getName().equals("price")) {  
  44.                     eventType = parser.next();  
  45.                     book.setPrice(Float.parseFloat(parser.getText()));  
  46.                 }  
  47.                 break;  
  48.             case XmlPullParser.END_TAG:  
  49.                 if (parser.getName().equals("book")) {  
  50.                     books.add(book);  
  51.                     book = null;      
  52.                 }  
  53.                 break;  
  54.             }  
  55.             eventType = parser.next();  
  56.         }  
  57.         return books;  
  58.     }  
  59.       
  60.     @Override  
  61.     public String serialize(List<Book> books) throws Exception {  
  62. //      XmlPullParserFactory factory = XmlPullParserFactory.newInstance();  
  63. //      XmlSerializer serializer = factory.newSerializer();  
  64.           
  65.         XmlSerializer serializer = Xml.newSerializer(); //由android.util.Xml创建一个XmlSerializer实例  
  66.         StringWriter writer = new StringWriter();  
  67.         serializer.setOutput(writer);   //设置输出方向为writer  
  68.         serializer.startDocument("UTF-8"true);  
  69.         serializer.startTag("""books");  
  70.         for (Book book : books) {  
  71.             serializer.startTag("""book");  
  72.             serializer.attribute("""id", book.getId() + "");  
  73.               
  74.             serializer.startTag("""name");  
  75.             serializer.text(book.getName());  
  76.             serializer.endTag("""name");  
  77.               
  78.             serializer.startTag("""price");  
  79.             serializer.text(book.getPrice() + "");  
  80.             serializer.endTag("""price");  
  81.               
  82.             serializer.endTag("""book");  
  83.         }  
  84.         serializer.endTag("""books");  
  85.         serializer.endDocument();  
  86.           
  87.         return writer.toString();  
  88.     }  
  89. }  

然后再对MainActivity做以下更改:

  1.         readBtn.setOnClickListener(new View.OnClickListener() {  
  2.             @Override  
  3.             public void onClick(View v) {  
  4.                 try {  
  5.                     InputStream is = getAssets().open("books.xml");  
  6. //                  parser = new SaxBookParser();  
  7. //                  parser = new DomBookParser();  
  8.                     parser = new PullBookParser();  
  9.                     books = parser.parse(is);  
  10.                     for (Book book : books) {  
  11.                         Log.i(TAG, book.toString());  
  12.                     }  
  13.                 } catch (Exception e) {  
  14.                     Log.e(TAG, e.getMessage());  
  15.                 }  
  16.             }  
  17.         });  

和其他两个执行结果都一样。

对于这三种解析器各有优点,我个人比较倾向于PULL解析器,因为SAX解析器操作起来太笨重,DOM不适合文档较大,内存较小的场景,唯有PULL轻巧灵活,速度快,占用内存小,使用非常顺手。读者也可以根据自己的喜好选择相应的解析技术。

作者:Linda_hurt 发表于2016/11/18 20:23:07 原文链接
阅读:22 评论:0 查看评论

自定义九宫格解锁控件

$
0
0

前言

九宫格手势解锁已经是非常常见的手机解锁方式,支付宝等一些软件也都曾经使用过,感觉还是很高大上的。在github已经有很好的开源控件,大家可以去自己搜索,我自己写了一个自定义的九宫格控件,作为练习作业。

效果图

我不会做动图,就凑合看吧……
这里写图片描述

先贴自定义属性代码
<!-- 九宫格解锁-->
<declare-styleable name="DrawPointLineUnlockView">
<!-- 行数 -->
<attr name="columns" format="integer"/>
<!-- 列数 -->
<attr name="rows" format="integer"/>
<!-- 连线的宽度 -->
<attr name="lineWidth" format="dimension"/>
<!-- 九宫格的大小 -->
<attr name="pointRadius" format="dimension"/>
<!-- 九宫格的颜色 -->
<attr name="pointColor" format="color" />
</declare-styleable>

我把自定义属性的连线宽度,作为了九宫格的实心圆的半径和外面的空心圆的边框宽度。

贴代码

package lzp.com.interestlibrary.view;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.CornerPathEffect;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import java.util.ArrayList;

import lzp.com.interestlibrary.R;
import lzp.com.interestlibrary.view.bean.DrawPoint;

/**
 * Created by li.zhipeng on 16/11/17.
 * <p>
 * 九宫格解锁View
 * <p>
 * 可以定制颜色和九宫格的数量
 */

public class DrawPointLineUnlockView extends View {

    /**
     * 手势按下时的x坐标
     */
    private float xDown = -1;

    /**
     * 手势按下时的y坐标
     */
    private float yDown = -1;
    /**
     * 手势y移动时的x坐标
     */
    private float xMove = -1;

    /**
     * 手势移动时的y坐标
     */
    private float yMove = -1;

    /**
     * 画笔的宽度
     */
    private int lineWidth = 10;

    /**
     * 每一个九宫格的大小
     */
    private int pointRadius = 30;

    /**
     * 九宫格的横向个数
     */
    private int columeCount = 3;

    /**
     * 九宫格的行数
     */
    private int rowCount = 3;

    /**
     * 九宫格的颜色
     */
    private int pointColor = Color.parseColor("#000000");

    /**
     * 画笔
     */
    private Paint paint;

    /**
     * 保存所有的九宫格的点的数组
     */
    private ArrayList<DrawPoint> pointsList;

    /**
     * 已经绘制的九宫格的点
     */
    private ArrayList<DrawPoint> drawPoints;

    /**
     * 进行判断的点
     * <p>
     * 只创建一个,不返回的创建对象,节省内存
     */
    private DrawPoint tempPoint;

    /**
     * 是否要进行绘制
     */
    private boolean needDraw;

    /**
     * 密码
     * */
    private String passward;

    /**
     * 解锁解锁回调
     * */
    private OnDrawPointUnlockResultListener listener;

    public void setPassward(String passward){
        this.passward = passward;
    }

    public DrawPointLineUnlockView(Context context) {
        this(context, null);
    }

    public DrawPointLineUnlockView(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 获取自定义属性的值
        TypedArray typedArray = getResources().obtainAttributes(attrs, R.styleable.DrawPointLineUnlockView);
        // 画笔的宽度
        lineWidth = typedArray.getDimensionPixelSize(R.styleable.DrawPointLineUnlockView_lineWidth, 10);
        // 每一个九宫格的大小
        pointRadius = typedArray.getDimensionPixelSize(R.styleable.DrawPointLineUnlockView_pointRadius, 30);
        // 九宫格的横向个数
        columeCount = typedArray.getInt(R.styleable.DrawPointLineUnlockView_columns, 3);
        // 九宫格的列数
        rowCount = typedArray.getInt(R.styleable.DrawPointLineUnlockView_rows, 3);
        // 九宫格的行数
        pointColor = typedArray.getColor(R.styleable.DrawPointLineUnlockView_pointColor, Color.parseColor("#000000"));
        typedArray.recycle();

        // 防止View不进行绘制,执行onDraw()方法
        setWillNotDraw(false);

        pointsList = new ArrayList<>();
        drawPoints = new ArrayList<>();
        tempPoint = new DrawPoint(-1, pointRadius, lineWidth);
        // 初始化画笔
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(pointColor);
        paint.setStrokeWidth(lineWidth);
        paint.setPathEffect(new CornerPathEffect(5));
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                xDown = event.getX();
                yDown = event.getY();
                // 判断是否这个点是九宫格的某一个点,否则不进行绘制
                tempPoint.x = xDown;
                tempPoint.y = yDown;
                int index = pointsList.indexOf(tempPoint);
                if (index != -1) {
                    needDraw = true;
                    DrawPoint point = pointsList.get(index);
                    drawPoints.add(point);
                    xDown = point.x;
                    yDown = point.y;
                } else {
                    needDraw = false;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (needDraw) {
                    xMove = event.getX();
                    yMove = event.getY();
                    // 判断是否这个点是九宫格的某一个点,如果是的话,连接这个点
                    tempPoint.x = xMove;
                    tempPoint.y = yMove;
                    int moveIndex = pointsList.indexOf(tempPoint);
                    // 判断这个点是否是九宫格的点  且 这个点并没有被连接过
                    if (moveIndex != -1 && !drawPoints.contains(tempPoint)) {
                        DrawPoint point = pointsList.get(moveIndex);
                        drawPoints.add(point);
                    }
                    invalidate();
                }
                break;
            case MotionEvent.ACTION_UP:
                if (needDraw) {
                    xDown = -1;
                    yDown = -1;
                    xMove = -1;
                    yMove = -1;
                    // 监听回调
                    if (listener != null){
                        String unlockResult = returnPwd();
                        listener.onUnlock(unlockResult, unlockResult.equals(passward));
                    }
                    // 清除所有的连接点
                    drawPoints.clear();
                    // 重绘
                    invalidate();
                }
                break;
        }
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // 画出所有的九宫格的点
        for (DrawPoint point : pointsList) {
            point.draw(canvas, paint);
        }
        // 如果正在解锁且已经划过了某些九宫格,画出他们之间的连线
        int size = drawPoints.size() - 1;
        if (needDraw && size >= 0) {
            // 只有一个起点,画出第一个点和手指位置的连线
            if (size == 0) {
                DrawPoint point = drawPoints.get(0);
                canvas.drawLine(point.x, point.y, xMove, yMove, paint);
            }
            // 多个点,画出点和点之间的连线
            else {
                for (int i = 0; i < size; i++) {
                    DrawPoint point = drawPoints.get(i);
                    DrawPoint nextPoint = drawPoints.get(i + 1);
                    canvas.drawLine(point.x, point.y, nextPoint.x, nextPoint.y, paint);
                }
                // 画出最后一个点和当前手指位置之间的连线
                DrawPoint point = drawPoints.get(size);
                canvas.drawLine(point.x, point.y, xMove, yMove, paint);
            }
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        // 测量当前的宽高 来绘制指定个数的九宫格
        float width = getWidth() - pointRadius * 2;
        float height = getHeight() - pointRadius * 2;

        // 获取padding值
        float paddingLeft = getPaddingLeft();
        float paddingRight = getPaddingRight();
        float paddingTop = getPaddingTop();
        float paddingBottom = getPaddingBottom();


        // 求出每个点的起始位置
        float xDistance = (width - paddingTop - paddingBottom) / (columeCount - 1);
        float yDistance = (height - paddingLeft - paddingRight) / (rowCount - 1);

        // 计算每一个九宫格的圆心
        for (int i = 0; i < rowCount; i++) {
            for (int j = 0; j < columeCount; j++) {
                DrawPoint pointF = new DrawPoint(j + rowCount * i, pointRadius, lineWidth);
                pointF.y = yDistance * i + pointRadius + paddingTop;
                pointF.x = xDistance * j + pointRadius + paddingLeft;
                pointsList.add(pointF);
            }
        }
    }

    /**
     * 返回密码
     * */
    private String returnPwd(){
        StringBuilder builder = new StringBuilder();
        for (DrawPoint point : drawPoints){
            builder.append(point.getValue());
        }
        return builder.toString();
    }

    /**
     * 解锁回调
     * */
    public interface OnDrawPointUnlockResultListener{
        /**
         *  @param passward 解锁的结果密码
         *  @param success 与设置的密码进行匹配的结果
         * */
        void onUnlock(String passward, boolean success);
    }

    public void setOnDrawPointUnlockResultListener(OnDrawPointUnlockResultListener listener){
        this.listener = listener;
    }
}

注释写的都是很清楚,不用做过多的解释,关于计算每一个九宫格的中心点,就画图说明一下
这里写图片描述

图太难画了 ,我们首先减去了九宫格的直径,这样均分了之后,我们就可以得到每一个九宫格的最左边的点,然后用最左边的点加上了半径就得到了x方向的圆心。在y方向上同理。

MainActivity:

DrawPointLineUnlockView drawPointLineUnlockView = (DrawPointLineUnlockView) findViewById(R.id.lock);
        drawPointLineUnlockView.setOnDrawPointUnlockResultListener(new DrawPointLineUnlockView.OnDrawPointUnlockResultListener() {
            @Override
            public void onUnlock(String passward, boolean success) {
                Toast.makeText(MainActivity.this, passward, Toast.LENGTH_SHORT).show();
            }
        });

结尾

如果有什么问题和好的建议欢迎大家留言批评指正,尤其是妹纸。

作者:u011315960 发表于2016/11/18 20:34:22 原文链接
阅读:32 评论:0 查看评论

手把手教您使用第三方登录

$
0
0

 今天讲一下第三方登录

市面很多应用都有登录注册功能,有的公司自己建立服务器,自己完成功能。但是现在有一些公司为了节省开发时间,会选择第三方登录。回想自己刚学安卓那会,就想怎么才能实现登录注册,甚至傻傻的建立本地数据库,自己写加密算法加密用户名密码等等,殊不知安全问题得不到好的解决,效率也是低下。这也是开篇写第三方登录的原因。

主流第三方登录包括:微信、qq、网盘、短信等,该文章基于网盘登录,主流登录各大博客都有很详细的介绍,楼主也是读了好几篇相关博客才敢写这本文章。限于篇幅原因,暂时用不到核心的登录功能,就暂时讲一下微盘登录。但核心方式大同小异。网盘登录相对比较简单,也算拓展一下视野。虽然,网盘官网现在已经停用了,但是这个技术点还是比较有意义的,写在这里作为了解。

如果是实际开发,你需要到指定第三方平台,注册账号成为开发者,然后创建属于本公司的应用,平台会给你一个app Key和app Secret,这个东西非常重要。本篇直接使用官方Demo里面的这两个值,就不做申请过程了。

下载官方提供的lib和Demo。我给您打包好了,可以直接下载:http://download.csdn.net/my

那么先运行官网Demo,看看啥子效果:


是的,您没看错,就是曾经辉煌一时的新浪网盘,我们就借用新浪的登录功能,成为我们项目的一部分,人家是大公司,内部机制和保护错失肯定比自己实现起来靠谱的多吧。

接下来就开始代码编写了,手把手教您在自己的IDE中跑起来。

PS:如果您闲一步步的操作麻烦、繁琐,可以直接复制最终那个完整代码,跑起来也可,主要还是为了了解技术点嘛。

1、关联下载好的lib包。

2、新建一个LoginActivity,布局只用一个Button用于授权登录(作为点击事件跳转登录界面)。代码如下:

/****省略导包****/

public class LoginActivity extends Activity {
    private Button mbtnLogin;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        mbtnLogin = (Button) findViewById(R.id.bt_login);
        mbtnLogin.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                //事件用于授权登录

            }
        });
    }
}

2、通过search功能在官方Demo找到授权登录的代码:

// 使用微盘Token认证,需设置重定向网址
// Need to set REDIRECT_URL if you want to use VDisk token.
session.setRedirectUrl(REDIRECT_URL);
session.authorize(OAuthActivity.this, OAuthActivity.this);

这块代码就是授权登录,也是核心一部分。我们加到自己Demo中:

            @Override
            public void onClick(View v) {
                //事件用于授权登录
                // 使用微盘Token认证,需设置重定向网址
                // Need to set REDIRECT_URL if you want to use VDisk token.
                session.setRedirectUrl(REDIRECT_URL);
                session.authorize(OAuthActivity.this, OAuthActivity.this);
            }
        });

两行代码就是授权了,只要执行完这条代码,就会通过新浪服务器的授权(对于授权是什么,后面学的深入了肯定会讲解相关的知识)。session.setRedirectUrl(REDIRECT_URL);参数也是您官网注册开发者账号的时候,自己建立的一个网址,这个问题不大,使用官方Demo中的即可。看注释写的也很清楚:Need to set REDIRECT_URL if you want to use VDisk token.

session.authorize(LoginActivity.this, LoginActivity.this);发起授权请求,让用户输入账号/密码。只要两行代码成功,就会回到onComplete(Bundle values)方法。第一个参数是一个上下文,第二个参数是授权通过后的监听器。直接使用自己的活动就可以了,例如我的主活动LoginActivity,因而我们的活动还要实现这个监听回调方法。

/**********   2、处理授权结果——>>得到accessToken    **********/
    @Override
    public void onComplete(Bundle values) {
        //授权完成的回调,这里使我们关心的,即授权成功后我们要干嘛

    }

    @Override
    public void onError(VDiskDialogError error) {
        //授权出错
    }

    @Override
    public void onVDiskException(VDiskException exception) {
        //抽取按过程中有异常(不是错误)
    }

    @Override
    public void onCancel() {

待会回来再讨论回调方法。

您会发现项目报错了,肯定的,因为你的好多对象没有初始化完毕。继续在Demo中拷贝初始化的代码。

以及上面所说的app Key、app Secret和REDIRECT_URL等

代码如下:

public class LoginActivity extends Activity implements VDiskDialogListener {
    private Button mbtnLogin;
    private VDiskAuthSession session;
    /**
     * 替换为开发者应用的appkey,例如"16*****960";
     * 
     * Replace it to the appkey of developer's application, such as
     * "16*****960";
     */
    public static final String CONSUMER_KEY = "2330724462";// TODO

    /**
     * 替换为开发者应用的app secret,例如"94098*****************861f9";
     * 
     * Replace it to the app secret of developer's application, such as
     * "94098*****************861f9";
     */
    public static final String CONSUMER_SECRET = "04f81fc56cc936bfc8f0fa1cef285158";// TODO

    /**
     * 替换为微博的access_token. 如果你想使用微博token直接访问微盘的API,这个字段不能为空。
     * 
     * Replace it to the access_token of WEIBO. If you use weibo token to access
     * VDisk API, this field should not be null.
     */
    public static String WEIBO_ACCESS_TOKEN = "WEIBO_ACCESS_TOKEN";
    /**
     * 
     * 此处应该替换为与appkey对应的应用回调地址,对应的应用回调地址可在开发者登陆新浪微盘开发平台之后,进入"我的应用--编辑应用信息--回调地址"
     * 进行设置和查看,如果使用微盘token登陆的话, 应用回调页不可为空。
     * 
     * The content of this field should replace with the application's redirect
     * url of corresponding appkey. Developers can login in Sina VDisk
     * Development Platform and enter "我的应用--编辑应用信息--回调地址" to set and view the
     * corresponding application's redirect url. If you use VDisk token, the
     * redirect url should not be empty. should not be empty.
     */
    private static final String REDIRECT_URL = "http://vauth.appsina.com/callback1.php";// TODO

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        mbtnLogin = (Button) findViewById(R.id.bt_login);

        /**
         * 初始化 Init
         */
        AppKeyPair appKeyPair = new AppKeyPair(CONSUMER_KEY, CONSUMER_SECRET);
        /**
         * @AccessType.APP_FOLDER - sandbox 模式
         * @AccessType.VDISK - basic 模式
         */

        session = VDiskAuthSession.getInstance(this, appKeyPair,
                AccessType.VDISK);


        mbtnLogin.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                //事件用于授权登录
                // 使用微盘Token认证,需设置重定向网址
                // Need to set REDIRECT_URL if you want to use VDisk token.
                session.setRedirectUrl(REDIRECT_URL);
                //1、发起授权请求,让用户输入账号/密码
                session.authorize(LoginActivity.this, LoginActivity.this);
            }
        });
    }

    /**********   2、处理授权结果——>>得到accessToken    **********/
    @Override
    public void onComplete(Bundle values) {
        //授权完成的回调,这里使我们关心的,即授权成功后我们要干嘛

    }

    @Override
    public void onError(VDiskDialogError error) {
        //授权出错
    }

    @Override
    public void onVDiskException(VDiskException exception) {
        //抽取按过程中有异常(不是错误)
    }

    @Override
    public void onCancel() {
        //授权取消
    }
}

还是,不要觉得繁琐,直接拷贝就行。

最后在再看看监听回调:

总共四个方法,注释写的很清楚了。我们最关心的,也就是

public void onComplete(Bundle values) {}了,一旦走到这里,就是我们要干的事情了,例如启动服务、跳转页面等等。注意的是,我们要在这里面处理服务器返回的accessToken,处理最终结果。还是去官方Demo中看如何处理最后授权结果吧。

@Override
    public void onComplete(Bundle values) {

        if (values != null) {
            AccessToken mToken = (AccessToken) values
                    .getSerializable(VDiskAuthSession.OAUTH2_TOKEN);
            session.finishAuthorize(mToken);
        }

        startActivity(new Intent(this, VDiskTestActivity.class));
        finish();
    }

我们处理完了accessToken,就是做了自己的事情——跳转到MainActivity。

对了,记得LoginActivity要在配置文件作为主活动、添加配置MainActivity哦。可以运行起来看看高大上的结果了~


额,没错。程序崩溃了,哈哈。因为你没有配置相应的权限,这些权限是啥,鬼知道是什么,但是官方Demo中有啊,去拷贝吧!

    <uses-permission android:name="android.permission.INTERNET" >
    </uses-permission>
    <uses-permission android:name="android.permission.READ_PHONE_STATE" >
    </uses-permission>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" >
    </uses-permission>
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" >
    </uses-permission>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" >
    </uses-permission>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

这个时候运行起来,可以完美实现微盘登录了,而且,我们把它的登录功能,变成了自己的,的确高大上,略显牛逼了吧。


到此第三方登录讲解完毕,后续参与实际开发用到更多核心功能的时候,还会去更细致的写这方面的文章,看在苦劳的份上,留下您的脚印,关注我哈。

您看一看文章也就5-10分钟,笔者要花1个多小时才能完成,喜欢我的朋友在下面留言点赞,关注我一起讨论问题哈。


长按上方二维码,关注本公众号每天更新一篇安卓文章

或者加入开发技术交流群:497646615

作者:qq_32059827 发表于2016/11/18 20:42:19 原文链接
阅读:38 评论:0 查看评论

USB驱动函数总结

$
0
0

pipe 管道

  管道是USB设备通信的通道,内核中提供了创建管道的宏,从宏中我们可以分析出,管道是一个 int 型的变量,由设备号、端点地址、端点类型组合而成。

usb_[snd|rcv][ctrl|int|bulk|isoc]pipe(dev, endpoint)
例:
struct usb_device *dev = interface_to_usbdev(intf);
struct usb_endpoint_descriptor *endpoint;
int pipe = usb_rcvintpipe(dev, endpoint->bEndpointAddress);
#define usb_sndctrlpipe(dev,endpoint)   \
    ((PIPE_CONTROL << 30) | __create_pipe(dev, endpoint))
#define usb_rcvctrlpipe(dev,endpoint)   \
    ((PIPE_CONTROL << 30) | __create_pipe(dev, endpoint) | USB_DIR_IN)
#define usb_sndisocpipe(dev,endpoint)   \
    ((PIPE_ISOCHRONOUS << 30) | __create_pipe(dev, endpoint))
#define usb_rcvisocpipe(dev,endpoint)   \
    ((PIPE_ISOCHRONOUS << 30) | __create_pipe(dev, endpoint) | USB_DIR_IN)
#define usb_sndbulkpipe(dev,endpoint)   \
    ((PIPE_BULK << 30) | __create_pipe(dev, endpoint))
#define usb_rcvbulkpipe(dev,endpoint)   \
    ((PIPE_BULK << 30) | __create_pipe(dev, endpoint) | USB_DIR_IN)
#define usb_sndintpipe(dev,endpoint)    \
    ((PIPE_INTERRUPT << 30) | __create_pipe(dev, endpoint))
#define usb_rcvintpipe(dev,endpoint)    \
    ((PIPE_INTERRUPT << 30) | __create_pipe(dev, endpoint) | USB_DIR_IN)
static inline unsigned int __create_pipe(struct usb_device *dev,
        unsigned int endpoint)
{
    return (dev->devnum << 8) | (endpoint << 15);
}

URB

分配URB

 usb_alloc_urb(int iso_packets, gfp_t mem_flags)

填充URB

控制传输

static inline void usb_fill_control_urb(struct urb *urb,
                    struct usb_device *dev,
                    unsigned int pipe,
                    unsigned char *setup_packet,
                    void *transfer_buffer,
                    int buffer_length,
                    usb_complete_t complete_fn,
                    void *context)
{
    urb->dev  = dev;
    urb->pipe = pipe;
    urb->setup_packet = setup_packet;
    urb->transfer_buffer = transfer_buffer;
    urb->transfer_buffer_length = buffer_length;
    urb->complete = complete_fn;
    urb->context  = context;
}

中断传输

static inline void usb_fill_int_urb(struct urb *urb,
                    struct usb_device *dev,
                    unsigned int pipe,
                    void *transfer_buffer,
                    int buffer_length,
                    usb_complete_t complete_fn,
                    void *context,
                    int interval)
{
    urb->dev  = dev;
    urb->pipe = pipe;
    urb->transfer_buffer = transfer_buffer;
    urb->transfer_buffer_length = buffer_length;
    urb->complete = complete_fn;
    urb->context = context;
    if (dev->speed == USB_SPEED_HIGH)   // 相比批量传输和控制传输,实时传输和中断传输多这个参数,表示周期
        urb->interval = 1 << (interval - 1);
    else
        urb->interval = interval;
    urb->start_frame = -1;
}

批量传输

static inline void usb_fill_bulk_urb(struct urb *urb,
                     struct usb_device *dev,
                     unsigned int pipe,
                     void *transfer_buffer,
                     int buffer_length,
                     usb_complete_t complete_fn,
                     void *context)
{
    urb->dev = dev;
    urb->pipe = pipe;
    urb->transfer_buffer = transfer_buffer;
    urb->transfer_buffer_length = buffer_length;
    urb->complete = complete_fn;
    urb->context = context;
}

等时传输

  不幸的是,等时urb 没有和中断、控制、批量urb 类似的初始化函数,因此它们在提交到USB核心之前,需要在驱动程序中手动的初始化。例如:

urb->dev = dev;
urb->context = uvd;
urb->pipe = usb_rcvisocpipe(dev,uvd->video_endp-1);
urb->interval = 1;
urb->transfer_flags = URB_IOS_ASAP;
urb->transfer_buffer = can->sts_buf[i];
urb_complete = konicawc_isoc_irq;
urb->number_of_packets = FRAMES_PRE_DESC;
urb->transfer_buffer_lenth = FRAMES_PRE_DESC;
for (j=0; j < FRAMES_PRE_DESC; j++){
    urb->ios_frame_desc[j].offset  = j;
    urb->ios_frame_desc[j].length  = 1;
}  

同步提交URB

  有时候 USB 驱动程序只是要发送或者接收一些简单的 USB 数据,而不是想创建一个 struct urb ,初始化它,然后等待该 urb 接收函数运行这些麻烦事都走一遍。内核提供了同步提交 urb 的接口。

控制传输

int usb_control_msg(struct usb_device *dev, 
                    unsigned int pipe, 
                    __u8 request,
                    __u8 requesttype, 
                    __u16 value, 
                    __u16 index, 
                    void *data,
                    __u16 size, 
                    int timeout  // 超时时间,以jiffies为单位
                    )

  如果函数调用成功,返回值为 0 ,如果返回一个负数,表示发生一个错误。如果成功 actual_lenth 参数包含从该消息发送或者接收的字节数。
举例:

/* usb_get_descriptor */
    result = usb_control_msg(dev, usb_rcvctrlpipe(dev, 0),
            USB_REQ_GET_DESCRIPTOR, USB_DIR_IN,
            (type << 8) + index, 0, buf, size,
            USB_CTRL_GET_TIMEOUT);

  控制传输,通过 pipe 可以看出传输方向是设备到主机,和默认端点0通信,请求类型是请求描述符,具体的类型和索引。传输完成时,描述符就被存放在 buf 所指向的缓冲区中了。

usb_ctrl_msg 分析

int usb_control_msg(struct usb_device *dev, 
                    unsigned int pipe, 
                    __u8 request,
                    __u8 requesttype, 
                    __u16 value, 
                    __u16 index, 
                    void *data,
                    __u16 size, 
                    int timeout)
{
    struct usb_ctrlrequest *dr;
    int ret;

    dr = kmalloc(sizeof(struct usb_ctrlrequest), GFP_NOIO);
    if (!dr)
        return -ENOMEM;
    // 填充 setup packet
    dr->bRequestType = requesttype;
    dr->bRequest = request;
    dr->wValue   = cpu_to_le16(value);
    dr->wIndex   = cpu_to_le16(index);
    dr->wLength  = cpu_to_le16(size);

    /* dbg("usb_control_msg"); */

    ret = usb_internal_control_msg(dev, pipe, dr, data, size, timeout);

    kfree(dr);

    return ret;
}
static int usb_internal_control_msg(struct usb_device *usb_dev,
                                    unsigned int pipe,
                                    struct usb_ctrlrequest *cmd,
                                    void *data, 
                                    int len, 
                                    int timeout)
{
    struct urb *urb;
    int retv;
    int length;

    urb = usb_alloc_urb(0, GFP_NOIO);
    if (!urb)
        return -ENOMEM;

    usb_fill_control_urb(urb, usb_dev, pipe, (unsigned char *)cmd, data,
                 len, usb_api_blocking_completion, NULL);

    retv = usb_start_wait_urb(urb, timeout, &length);
    if (retv < 0)
        return retv;
    else
        return length;
}
static inline void usb_fill_control_urb(struct urb *urb,
                    struct usb_device *dev,
                    unsigned int pipe,
                    unsigned char *setup_packet,
                    void *transfer_buffer,
                    int buffer_length,
                    usb_complete_t complete_fn,
                    void *context)
{
    urb->dev  = dev;
    urb->pipe = pipe;
    urb->setup_packet = setup_packet;
    urb->transfer_buffer = transfer_buffer;
    urb->transfer_buffer_length = buffer_length;
    urb->complete = complete_fn;
    urb->context  = context;
}
struct api_context {
    struct completion   done;
    int         status;
};

static int usb_start_wait_urb(struct urb *urb, int timeout, int *actual_length)
{
    struct api_context ctx;
    unsigned long expire;
    int retval;
    // 完成量
    init_completion(&ctx.done);
    urb->context = &ctx;
    urb->actual_length = 0;
    retval = usb_submit_urb(urb, GFP_NOIO);
    if (unlikely(retval))
        goto out;

    expire = timeout ? msecs_to_jiffies(timeout) : MAX_SCHEDULE_TIMEOUT;
    // 等待完成,返回值为0表示超时
    if (!wait_for_completion_timeout(&ctx.done, expire)) {
        usb_kill_urb(urb);
        retval = (ctx.status == -ENOENT ? -ETIMEDOUT : ctx.status);

        dev_dbg(&urb->dev->dev,
            "%s timed out on ep%d%s len=%u/%u\n",
            current->comm,
            usb_endpoint_num(&urb->ep->desc),
            usb_urb_dir_in(urb) ? "in" : "out",
            urb->actual_length,
            urb->transfer_buffer_length);
    } else
        retval = ctx.status;
out:
    if (actual_length)
        *actual_length = urb->actual_length;

    usb_free_urb(urb);
    return retval;
}
static void usb_api_blocking_completion(struct urb *urb)
{
    struct api_context *ctx = urb->context;

    ctx->status = urb->status;
    complete(&ctx->done);
}

  不难分析,内核帮助我们分配设置了 urb ,并且提供了一个统一的完成函数,在提交 urb 时有一点需要注意,内核初始化了一个完成量,并且内核在提交 urb 之后在 wait_for_completion_timeout ,等待的过程中线程自然就休眠了,何时完成呢?在统一的完成函数中 complete(&ctx->done) 。所以称这种方式为同步提交 urb 。

中断、批量传输

int usb_interrupt_msg(...)
{
  return usb_bulk_msg(usb_dev, pipe, data, len, actual_length, timeout);
}
int usb_bulk_msg(struct usb_device *usb_dev, 
unsigned int pipe,
         void *data, 
int *actual_length, 
int timeout
)

批量和控制传输的参数简单一点,举例:

/* 进行阻塞的批量读取,从设备获取数据 */
retval = usb_bulk_msg(dev->udev,
    usb_rcvbulkpipe(dev->udev,dev->bulk_in_endpointAddr),
    dev->bulk_in_buffer,
    min(dev->bulk_in_size,count),
    &count,
    HZ*10
)
/* 如果读取成功 */
if (!retval){
    if (copy_to_user(buffer, dev->bulk_in_buffer, count))
        retval = -EFAULT;
    else
        retval = count;
}

  批量传输,传输方向设备到主机,和端点bulk_in_endpoint通信。

usb_bulk_msg分析

int usb_bulk_msg(struct usb_device *usb_dev, unsigned int pipe,
         void *data, int len, int *actual_length, int timeout)
{
    struct urb *urb;
    struct usb_host_endpoint *ep;

    // 在 usb_device 的 ep_in[] 和 ep_out[] 数组中找到管道对应的断点
    ep = (usb_pipein(pipe) ? usb_dev->ep_in : usb_dev->ep_out)
            [usb_pipeendpoint(pipe)];
    if (!ep || len < 0)
        return -EINVAL;

    // 分配 urb
    urb = usb_alloc_urb(0, GFP_KERNEL);
    if (!urb)
        return -ENOMEM;

    // 如果是中断端点
    if ((ep->desc.bmAttributes & USB_ENDPOINT_XFERTYPE_MASK) ==
            USB_ENDPOINT_XFER_INT) {
        pipe = (pipe & ~(3 << 30)) | (PIPE_INTERRUPT << 30);
        usb_fill_int_urb(urb, usb_dev, pipe, data, len,
                usb_api_blocking_completion, NULL,
                ep->desc.bInterval);
    } else  // 如果是 bulk 端点
        usb_fill_bulk_urb(urb, usb_dev, pipe, data, len,
                usb_api_blocking_completion, NULL);

    return usb_start_wait_urb(urb, timeout, actual_length);
}

异步提交urb

  异步提交 urb 需要我们自己去创建、设置、提交urb,并且提供一个完成函数。
举例:

static int usb_mouse_probe(struct usb_interface *intf, const struct usb_device_id *id){

    struct usb_device *dev = interface_to_usbdev(intf);
    struct usb_host_interface *interface;
    struct usb_endpoint_descriptor *endpoint;
    int pipe;
    interface = intf->cur_altsetting;
    endpoint = &interface->endpoint[0].desc; //endpoint = &intf->cur_altsetting->endpoint[0].desc

    pipe = usb_rcvintpipe(dev, endpoint->bEndpointAddress);
    len = endpoint->wMaxPacketSize;
    //分配缓冲区,dma
    usb_buf = usb_buffer_alloc(dev, len, GFP_ATOMIC, &usb_buf_phys);
    //分配urb
    um_urb = usb_alloc_urb(0, GFP_KERNEL);
    //填充urb
    usb_fill_int_urb(um_urb, dev, pipe, usb_buf, len, usb_mouse_irq, NULL, endpoint->bInterval);

    um_urb->transfer_dma = usb_buf_phys;
    um_urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;  //使用dma传输

    /* 使用URB */
    usb_submit_urb(um_urb, GFP_KERNEL);

    return 0;
}
static void usb_mouse_irq(struct urb *urb)
{
    static unsigned char pre_val;

    /* USB鼠标数据含义
     * data[0]: bit0-左键, 1-按下, 0-松开
     *          bit1-右键, 1-按下, 0-松开
     *          bit2-中键, 1-按下, 0-松开 
     *
     */
    if ((pre_val & (1<<0)) != (usb_buf[0] & (1<<0)))
    {
        /* 左键发生了变化 */
        input_event(um_dev, EV_KEY, KEY_L, (usb_buf[0] & (1<<0)) ? 1 : 0);
        input_sync(um_dev);
    }

    if ((pre_val & (1<<1)) != (usb_buf[0] & (1<<1)))
    {
        /* 右键发生了变化 */
        input_event(um_dev, EV_KEY, KEY_S, (usb_buf[0] & (1<<1)) ? 1 : 0);
        input_sync(um_dev);
    }

    if ((pre_val & (1<<2)) != (usb_buf[0] & (1<<2)))
    {
        /* 中键发生了变化 */
        input_event(um_dev, EV_KEY, KEY_ENTER, (usb_buf[0] & (1<<2)) ? 1 : 0);
        input_sync(um_dev);
    }

    pre_val = usb_buf[0];

    /* 重新提交urb */
    usb_submit_urb(um_urb, GFP_KERNEL);
}

销毁URB

usb_free_urb(struct urb * urb)

取消URB

  应该调用 usb_fill_urb 或 usb_unlink_urb 函数来终止一个已经被提交到USB核心的urb。
  如果调用 usb_kill_urb ,urb 的生命周期将终止,通常是当设备从系统中断开时,在断开回调函数中调用此函数。
  对于某些驱动程序而言,应该使用 usb_ublink_urb 函数来告诉USB核心终止一个urb,该函数并不等到urb完全被终止之后才返回回调函数。这对于在中断处理例程中或者持有一个自旋锁时终止一个urb是很有用,因为等待一个urb完全被终止需要USB核心具有使调用进程休眠的能力。该函数需要被要求终止的urb中的URB_ASYNC_UNLINK标志值被设置才能正确地工作。

作者:lizuobin2 发表于2016/11/18 20:46:47 原文链接
阅读:42 评论:0 查看评论

TableView的accessoryButtonTappedForRow方法执行的时机

$
0
0

敲代码时遇到了这个问题,别偷懒,写下来备查.

当你在IB中对TableView中的accessory(注意,我说的是cell中的accessory,而不是cell)创建segue时,如果你在VC中同时实现以下3个方法,请问调用的次序是神马!?

//1
func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath)

//2
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool

//3
override func prepare(for segue: UIStoryboardSegue, sender: Any?) 

答案是:2,1,3

如果你使用shouldPerformSegue,那么你在该方法里是无法取到segue本身的,你必须配合prepare方法才可以.

如果你需要在prepare方法里取得被按下accessory对应的cell的索引,那么你不可以使用tableView.indexPathForSelectedRow,因为你segue绑定的是cell的accessory而不是cell,所以你取得的返回值是nil.

要想解决以上问题非常简单prepare第二个参数sender就是了:

let cell = sender as! UITableViewCell
let selectedRow = tableView.indexPath(for: cell)!.row

如果你是自定义cell的accessory按钮,那么你可能回用得着下面这个方法:

tableView.indexPathForRow(at: point)

下面是等效的objC的代码,留个念想:

NSSet *touches = [event allTouches];  
UITouch *touch = [touches anyObject];  
CGPoint currentTouchPosition = [touch locationInView:self.contactsTableView];
作者:mydo 发表于2016/11/18 20:49:54 原文链接
阅读:139 评论:0 查看评论

大话Android Hybrid

$
0
0

前言

其实之前一直都很抵制hybrid开发,因为作为一个Android开发程序员,总是觉得原生的更好(其实是不想丢饭碗),但是一个闲着没事干,就写了一个demo搭了个webview,然后把html文件放到asset下面,一加载惊呆宝宝了,简直跟原生的没有区别啊,体验跟原生基本一样(andrid 5.0以后webview的速度比之前的版本有很大的提升),至此我就走上了学习混合开发的道路.

前期准备 WebView

其实我相信很多跟我一样刚入门混合开发的人,对于应该要学习哪部分知识都会感到迷惑,在这里我先谈谈我的经验:

  1. html基础,不用说很厉害,但是至少你要知道整个html的体系还有css,div等控件的使用,还有对html节点的一些基本操作
  2. javaScript基础,这里说的基础就是语法之类的,js这一部分其实挺重要的,跟上面一样,但是js你懂得越多,少走的坑也就真的越少,切身体会啊!!!
  3. 对一些常见的前段框架的运用,比如jquery,sui-mobile,第三点倒不是很重要
  4. webview的原理,这一点挺重要的,因为它涉及到webview中的js怎么去与android的native交互的原理,懂得原理你可以在混合开发中更加”自由地”做出你想要的东西

把网页搬到自己的app上面

假如我们的第一个需求是:把一个网页搬到自己的app上面。那此时,我们只需要下面这段代码就行

public class BolgActivity extends AppCompatActivity {

    private WebView webView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_bolg);
        webView = (WebView) findViewById(R.id.webview);
        //这段代码的作用是让webview不要使用系统自带浏览器
        webView.setWebViewClient(new WebViewClient(){
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url){
                return false;
            }
        });
        webView.loadUrl("http://www.baidu.com");
    }
}

运行结果:
图片名称
在一般情况下,我们的做法都是把html文件放在app的asset目录下,然后通过下面的代码来加载html文件,这样大大提高了加载速度

mWebView.loadUrl("file:///android_asset/index.html");

native提供帮助

很明显这不能满足我们的需求,这样你的应用和浏览器又有什么区别,那现在我们提高要求。假如现在我们有一个新的需求:
要求网页弹出一个加载框(要求不是网页的加载框,而是android系统原生的加载框)
在完成这个需求之前,就必须先讲讲webview中的页面怎么去和android原生交互了.我们可以先把webview想象成一个容器,那么html页面就是运行在这个容器上面的.
那么我们的webview想要去调用html页面里的js就通过下面这段代码

webView.loadUrl("javascript: log()");

那么js中要调用android里面的代码就是通过webview注入一个对象,然后让js调用,下面我用伪代码表示一下:

{
    //假设这里是activity里面
    webView.addJavascriptInterface(new JsInterface(), "interface");
    class JsInterface{
        void showToast(String msg){
            Toast.make(MainActivity.this, toast, Toast.LENGTH_SHORT).show();
        }   
    }
}
-------------------------------------------------------
{
    //假设这里是在一段JavaScript函数里面
    interface.showToast("I'm js");
}

上面讲的是第一种js和native交互的方式,其实我说的很直白,如果看不懂可以看看下面这篇博文:
js与webview的交互(并不是很喜欢这种转载的,但由于原创的那篇打不开,将就一下吧)

进阶 JsBridge

ps:上面我们讲了js与native交互的一种方案,但在实际运用中我发现并不是很好用,js与native的调用一多,会显得代码非常混乱,当然后面也有个方案能解决,甚至比我接下来要讲的方案还要好,这里先讲下我现在在使用的方案。
相信我的这篇blog是你查阅了大量资料以后才无意中看到的,那么你肯定是知道JsBridge了。先看看代码

{
    //这是在native中的代码,mWebView为JsBridge封装过的webview
    mWebView.callHandler("myInit", "我是数据", new CallBackFunction() {
            @Override
            public void onCallBack(String data) {

            }
        });
}
-----------------------------------------------------------------------------------------------------
{
    //这是在Js中的代码,bridge是native注入在js中的对象
    bridge.registerHandler("functionInJs", function(data, responseCallback) {
                document.getElementById("content").innerHTML = ("data from Java: = " + data);
                var responseData = "Javascript Says  我要你的地址!";
                responseCallback(responseData);
            });
}

上面这段代码什么意思呢?就是在js中通过bridge注册一个函数,然后在native中通过封装好的webview.callHandler函数来调用js中已经注册好的函数,那么反过来Js想调用native的函数,也是同样的道理,只要在native中注册好函数,在js中通过bridge.callHandler来调用就行了。
这样给我们的开发提供了极大的方便,而且避免了很多代码混乱的情况,那么它的基本原理是什么呢?下面我还是用最简单地语言来讲一讲。

  • 在我们的webview中,可以设置自定义的WebChromeClient,这个东西就是加载在webview中的html中有弹窗事件的时候会回调的
  • WebChromeClient里面有很多种回调,其中有一个回调方法onJsPrompt,这个方法就是我们的突入点(其他方法也都可以,但是可能会干扰到原来的逻辑,这个比较少用,所以我们用这个)

语言是不是很简单暴力?如果你还看不懂,看看下面的实例:

{
    //假设这里是在js里面
    var uri = "hybrid://objectName:sid/methodName?params";
    var value = "xxxxxxx";
    window.prompt(uri, value);
}
--------------------------------------------------------------
{
    //这是在activity里面
    public class InjectedChromeClient extends WebChromeClient {
            @Override
            public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
                //这里传进来的参数就是从js的window.prompt(uri, value)传过来的参数
                return super.onJsPrompt(view, url, message, defaultValue, result);
            }
    }
}

JsBridge的工作原理就是这样,那么现在看来好像对我们没什么用呀?下面我举个实际应用的例子说一下,就拿上面那个例子来说:要求网页弹出一个加载框(要求不是网页的加载框,而是android系统原生的加载框)
(先不要讲太复杂的代码,从上面JsBridge的知识我们已经知道了js里面能传递数据给webview)

{
    //假设这个方法是js中负责发送数据给webview的
    function postToNative(data){..省略..}
    var data = 
    {
        "tagName":"showLoadingDialog",
        "pars":{
            "content":"正在加载中",
            "title":"请等待",
            //这个callback就是当加载框被取消的时候native回调js中的函数
            "callback":"onLoadingCancel"
        }   
    };
    postToNative(data);
}

{
    //假设这个方法实在native中接收webview中的js发过来的数据
    void receiveJsData(String data){
        //上面js的函数可以看出,js向我们发送了一段json数据
        //所以我们只需解析这段数据,便可知道js需要native做什么事
    }
}

总结

对比

觉得上面讲的都是废话?
其实网上关于hybrid开发的文章我敢说我基本都看过了,国外的文章也有看了看,在实际开发中,我自己也写了很多demo,下面是我总结的关于一些JsBridge的优缺点:
优点

  1. 相比传统的方式方便很多,代码更加简洁
  2. 其实第一点已经完爆一切了,写不出第二点了哈哈

缺点
1. 调试困难,不知是否是博主基础太差,过程中有许多意料不到的错(当然是关于js的),不过到后面基本都能解决了
2. 如果要做到一个页面可以兼容所有web页面,那需要注册的函数太多了,因为每个页面需要的功能都不一样
3. 生命周期有点难掌控,假如你在JsBridge未初始化之前就调用,那么会导致JsBridge初始化失败(之前这个坑陷了好久后面才发现的)

实际应用

在实际开发中,我的方法是通过json来灵活地定义每个动作,也就是利用json数据,来告诉native我需要做什么动作。通过这种方式,一个app甚至只要一个activity就能解决所有的页面,此时有人要问了,那我每个页面的ui就都一样了吗?比如说toolbar,alertDialog。
然而并不是这样的,通过我上面将的知识点,可以在js初始化的时候,定义一段json数据,用户初始化ui,例如下面的:

var data =
{
    "title":"话题列表",
    "right":{
        "icon":"R.mipmap.ic_launcher",
        "description":"描述",
        "callback":"onRightBtnClick"
    }
};

然后在native中解析这段数据,来达到初始化ui的效果,这种方式我也已经运用在我的项目中。
相信你看了这边文章,你已经基本知道了webview在实际开发中的应用和各种方案,如果觉得文章有哪些地方写的不好,请留言说明。

ps:代码下次贴出,在写个几个基类供参考,写得还不是很完善,需要的也提出来把!

作者:qq122627018 发表于2016/11/18 20:53:14 原文链接
阅读:49 评论:0 查看评论

Android五子棋小游戏之UI篇

$
0
0

最近一直在学习Android自定义View方面的知识,正好看到一个讲解制作五子棋小游戏的案例,遂学习一番,记录下学习过程,帮助那些有需要的人。

首先放上效果图:
五子棋小游戏

下面我将带领大家一步步完成这个五子棋小游戏。

一、创建自定义View类及定义成员变量

首先我们先定义一个类WuziqiPanel,让该类继承自View,并在类中定义一些成员变量,便于我们后面使用,而且在我们需要显示五子棋的布局文件中引入该自定义View。

WuziqiPanel.java文件

public class WuziqiPanel extends View{
    //棋盘宽度
    private int mPanelWidth;
    //棋盘格子的行高(声明为int会造成由于不能整除而造成的误差较大)
    private float mLineHeight;
    //棋盘最大行列数(其实就是棋盘横竖线的个数)
    private int MAX_LINE_NUM = 10;

    //定义画笔绘制棋盘格子
    private Paint mPaint = new Paint();
    //定义黑白棋子Bitmap
    private Bitmap mWhitePiece;
    private Bitmap mBlackPiece;

    //棋子的缩放比例(行高的3/4)
    private float pieceScaleRatio = 3 * 1.0f / 4;

    //存储黑白棋子的坐标
    private ArrayList<Point> mWhiteArray = new ArrayList<>();
    private ArrayList<Point> mBlackArray = new ArrayList<>();
    //哪方先下子
    private boolean isWhiteFirst = true;

    //游戏是否结束
    private boolean isGameOver;
    //确定赢家
    private boolean isWhiteWinner = false;
    //游戏结束监听
    private OnGameOverListener onGameOverListener;
}

activity_main.xml文件中引入该自定义View

activity_main.xml文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/main_bg"
    tools:context="com.codekong.wuziqi.activity.MainActivity">

    <com.codekong.wuziqi.view.WuziqiPanel
        android:id="@+id/id_wuziqi_panel"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerInParent="true" />
</RelativeLayout>

注意:自定义View的引入必须使用完整路径

二、定义构造函数并初始化设置

这一步我们首先要书写构造函数,并且在构造函数中初始化一些设置。比如初始化画笔以及将棋子图片转为bitmap
这里我们在三个参数的构造方法中调用两个参数的构造方法,又在两个参数的构造方法中调用一个参数的构造方法。

这里简单解释一下。一个参数的构造方法是我们在new出一个组件的时候调用;两个参数的构造方法是我们在XML中使用自定义View时调用;三个参数的构造方法是我们自定义View中使用了自定义属性的时候调用;所以我们按上面的写法就可以覆盖到这三种情况。

public WuziqiPanel(Context context) {
    this(context, null);
}

public WuziqiPanel(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public WuziqiPanel(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
}

/**
 * 初始化设置
 */
private void init() {
    //初始化画笔
    mPaint.setColor(0x88000000);
    //设置抗锯齿
    mPaint.setAntiAlias(true);
    //设置防抖动
    mPaint.setDither(true);
    //设置为空心(画线)
    mPaint.setStyle(Paint.Style.STROKE);

    //初始化棋子
    mWhitePiece = BitmapFactory.decodeResource(getResources(), R.drawable.icon_white_piece);
    mBlackPiece = BitmapFactory.decodeResource(getResources(), R.drawable.icon_black_piece);
}

三、测量

测量几乎是自定义View必须要经历的步骤,由于我们要先绘制棋盘,所以我们必须先测量出我们需要的数据。
我们在onMeasure()方法中拿到屏幕宽高,然后在onSizeChanged()中获得棋盘的宽度,计算出棋盘的行高。接着根据行高缩放棋子大小,使其显示大小合适。

/**
 * 测量
 * @param widthMeasureSpec
 * @param heightMeasureSpec
 */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    int width = Math.min(widthSize, heightSize);

    //此处的逻辑判断是处理当我们自定义的View被嵌套在ScrollView中时,获得的测量模式
    // 会是UNSPECIFIED
    // 使得到的widthSize或者heightSize为0
    if (widthMode == MeasureSpec.UNSPECIFIED){
        width = heightSize;
    }else if (heightMode == MeasureSpec.UNSPECIFIED){
        width = widthSize;
    }
    //调用此方法使我们的测量结果生效
    setMeasuredDimension(width, width);
}
/**
 * 当宽高发生变化时回调此方法
 * @param w
 * @param h
 * @param oldw
 * @param oldh
 */
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    //此处的参数w就是在onMeasure()方法中设置的自定义View的大小
    //计算出棋盘宽度和行高
    mPanelWidth = w;
    mLineHeight = mPanelWidth * 1.0f / MAX_LINE_NUM;

    //将棋子根据行高变化
    int pieceWidth = (int) (pieceScaleRatio * mLineHeight);
    mWhitePiece = Bitmap.createScaledBitmap(mWhitePiece, pieceWidth, pieceWidth, false);
    mBlackPiece = Bitmap.createScaledBitmap(mBlackPiece, pieceWidth, pieceWidth, false);
}

注意:如上面注释所写,由于我们不知道我们的自定义View将会被放在什么样的布局中,所以如果我们的五子棋盘被放在ScrollView中,我们测量到的宽或者高就会有一方为,就会使我们的测量失效,从而影响后面的绘制,所以我们必须处理这一种情况.

四、绘制棋盘

首先我们应该先绘制好我们要下棋的棋盘,我们此处准备横竖都画10条线来绘制我们的棋盘,其实此处的棋盘的横竖线的个数我们是可以修改的,棋子的大小也会随着棋盘格子的大小缩放,但是为了美观一些,我们此处采用横竖10条线.

大家可以先通过我下面的示意图来理解一下棋盘横竖线坐标的确定.

棋盘绘制

/**
 * 绘制棋盘
 * @param canvas
 */
private void drawBoard(Canvas canvas) {
    int w = mPanelWidth;
    float lineHeight = mLineHeight;

    for (int i = 0; i < MAX_LINE_NUM; i++) {
        int startX = (int) (lineHeight / 2);
        int endX = (int) (w - lineHeight / 2);

        int y = (int) ((0.5 + i) * lineHeight);
        //画横线
        canvas.drawLine(startX, y, endX, y, mPaint);
        //画竖线
        canvas.drawLine(y, startX, y, endX, mPaint);
    }
}

通过上面的步骤棋盘就算是绘制好了.

五、处理用户手势-下棋

上面我们绘制好了棋盘,接着我们就可以下棋啦,所以理所当然我们要开始处理用户的手势,开始下棋啦,所以我们要重写onTouchEvent().

这一步我们要做三件事:

1 . onTouchEvent()return true拦截手势事件我们自己处理

2 . 获得用户触摸的坐标并进行处理,处理为棋盘上的整数值坐标,并存储起来.

这一步我们调用一个自定义的函数getValidPoint()将用户点击的点的坐标转化为整数值.也就是说用户下子的时候不必精确点击到棋盘各自的交叉点上,而是只要在这个交叉点周围就可以了,我们只需要将其取整后除以我们格子的高度(行高),

简单解释一下,如下图1、2所指的箭头所示,由于我们的棋盘上下左右边距为0.5倍的行高,当我们手指在以棋盘顶点0.5倍的行高范围内点击,只要除以行高取整,就会得到该顶点坐标.比如我点击的坐标点为(0.75,0.82),在第一个圈范围内,此时行高为,除以1后取整得到(0,0)就是棋盘的顶点,就是我们需要落子的地方。

坐标解释

/**
 * 将用户点击的位置的Point转换为类似于(0,0)d的坐标
 * @param x
 * @param y
 * @return
 */
private Point getValidPoint(int x, int y) {
    return new Point((int) (x / mLineHeight), (int) (y / mLineHeight));
}

3 . 调用invalidate()方法进行界面重绘,绘制出用户所下的棋子

每次调用invalidate()方法就会调用onDraw()方法进行界面绘制,在该方法中先绘制棋盘,然后绘制用户所下的棋子,还要判断游戏结束.绘制棋子和判断游戏结束我会在后面的步骤中给出介绍.

4 . 将下棋权利交于另一方(白棋下完换黑棋)

这一步比较好处理,我们只需要将一个布尔变量isWhiteFirst取反,下一次就轮到另一种棋子下子了.

/**
 * 处理用户手势操作
 * @param event
 * @return
 */
@Override
public boolean onTouchEvent(MotionEvent event) {
    if (isGameOver) return false;

    int action = event.getAction();
    //手指抬起后处理
    if (action == MotionEvent.ACTION_UP){

        //拦截事件自己来处理
        int x = (int) event.getX();
        int y = (int) event.getY();
        Point point = getValidPoint(x, y);
        //首先判断所点击的位置是不是已经有棋子
        if (mWhiteArray.contains(point) || mBlackArray.contains(point)){
            return false;
        }
        //白棋先下
        if (isWhiteFirst){
            mWhiteArray.add(point);
        }else{
            mBlackArray.add(point);
        }
        //调用重绘
        invalidate();
        isWhiteFirst = !isWhiteFirst;
    }
    return true;
}
/**
 * 进行绘制工作
 * @param canvas
 */
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //绘制棋盘
    drawBoard(canvas);
    //绘制用户已经下的所有棋子
    drawPieces(canvas);
    //判断游戏是否结束
    checkGameOver();
}

六、绘制棋子

上一步我们已经把用户所点击的要下棋子的坐标存储在了ArrayList,这一步我们就将遍历这个ArrayList将黑白棋子绘制到棋盘上.

这里我们的一个变量pieceScaleRatio = 3/4,表示一个棋子长宽为3/4的行高,剩余的1/4的行高平均棋子的左右各留出1/8的行高,这样棋子距离左右边框的距离为1/8行高,棋子与棋子之间的间距为2*1/8=1/4行高.

/**
 * 绘制棋子
 * @param canvas
 */
private void drawPieces(Canvas canvas) {
    //绘制白棋子
    for (int i = 0, n = mWhiteArray.size(); i < n; i++) {
        Point whitePoint = mWhiteArray.get(i);
        //棋子之间的间隔为1/4行高
        canvas.drawBitmap(mWhitePiece,
                (whitePoint.x + (1 - pieceScaleRatio) / 2) * mLineHeight,
                (whitePoint.y + (1 - pieceScaleRatio) / 2) * mLineHeight, null);
    }
    //绘制黑棋子
    for (int i = 0, n = mBlackArray.size(); i < n; i++) {
        Point blackPoint = mBlackArray.get(i);
        //棋子之间的间隔为1/4行高,棋子距离左右边框的距离为1/8行高
        canvas.drawBitmap(mBlackPiece,
                (blackPoint.x + (1 - pieceScaleRatio) / 2) * mLineHeight,
                (blackPoint.y + (1 - pieceScaleRatio) / 2) * mLineHeight, null);
    }
}

七、判断游戏结束

经过上面的步骤,我们已经可以在棋盘上落子了,下面的任务就是我们要判断游戏是否结束.
这个游戏的规则比较简单,我们只要判断在上下左右斜对角线如果存在连续5个棋子是同一色,我们就可以判定胜负了.

我们专门定义一个工具类WuziqiUtil.java来进行判断

public class WuziqiUtil {
    //每行上最大的数目
    public static final int MAX_COUNT_IN_LINE = 5;

    /**
     * 检查是否五子连珠
     * @param points
     * @return
     */
    public static boolean checkFiveInLine(List<Point> points) {
        for (Point p: points) {
            int x = p.x;
            int y = p.y;

            boolean win = checkHorizontal(x, y, points);
            if (win) return true;
            win = checkVertical(x, y, points);
            if (win) return true;
            win = checkLeftDiagonal(x, y, points);
            if (win) return true;
            win = checkRightDiagonal(x, y, points);
            if (win) return true;
        }
        return false;
    }

    /**
     * 判断x, y位置的棋子是否横向五个一致
     * @param x
     * @param y
     * @param points
     * @return
     */
    public static boolean checkHorizontal(int x, int y, List<Point> points) {
        int count = 1;
        for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {
            if (points.contains(new Point(x - i, y))){
                count ++;
            }else {
                break;
            }
        }

        if (count == MAX_COUNT_IN_LINE) return true;

        for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {
            if (points.contains(new Point(x + i, y))){
                count ++;
            }else {
                break;
            }
        }

        if (count == MAX_COUNT_IN_LINE) return true;
        return false;
    }

    /**
     * 判断x, y位置的棋子是否竖向五个一致
     * @param x
     * @param y
     * @param points
     * @return
     */
    public static boolean checkVertical(int x, int y, List<Point> points) {
        int count = 1;
        for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {
            if (points.contains(new Point(x, y - i))){
                count ++;
            }else {
                break;
            }
        }

        if (count == MAX_COUNT_IN_LINE) return true;

        for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {
            if (points.contains(new Point(x, y + i))){
                count ++;
            }else {
                break;
            }
        }

        if (count == MAX_COUNT_IN_LINE) return true;
        return false;
    }

    /**
     * 判断x, y位置的棋子是否左斜向上五个一致
     * @param x
     * @param y
     * @param points
     * @return
     */
    public static boolean checkLeftDiagonal(int x, int y, List<Point> points) {
        int count = 1;
        for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {
            if (points.contains(new Point(x - i, y + i))){
                count ++;
            }else {
                break;
            }
        }

        if (count == MAX_COUNT_IN_LINE) return true;

        for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {
            if (points.contains(new Point(x + i, y - i))){
                count ++;
            }else {
                break;
            }
        }

        if (count == MAX_COUNT_IN_LINE) return true;
        return false;
    }

    /**
     * 判断x, y位置的棋子是否右斜向下五个一致
     * @param x
     * @param y
     * @param points
     * @return
     */
    public static boolean checkRightDiagonal(int x, int y, List<Point> points) {
        int count = 1;
        for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {
            if (points.contains(new Point(x - i, y - i))){
                count ++;
            }else {
                break;
            }
        }

        if (count == MAX_COUNT_IN_LINE) return true;

        for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {
            if (points.contains(new Point(x + i, y + i))){
                count ++;
            }else {
                break;
            }
        }

        if (count == MAX_COUNT_IN_LINE) return true;
        return false;
    }
}

然后我们只要在在自定义View类中的checkGameOver()方法中进行调用就可以判断游戏是否结束

/**
 * 检查游戏是否结束
 */
private void checkGameOver() {
    //检查是否五子连珠
    boolean whiteWin = WuziqiUtil.checkFiveInLine(mWhiteArray);
    boolean blackWin = WuziqiUtil.checkFiveInLine(mBlackArray);
    if (whiteWin || blackWin){
        isGameOver = true;
        isWhiteWinner = whiteWin;

        //String msg = isWhiteWinner ? "白子获胜" : "黑子获胜";
        //Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
        onGameOverListener.gameOver(isWhiteWinner);
    }
}

或许已经有人发现了,我上面还声明了一个游戏结束的监听,对的,我们作为一个自定义View当然要将游戏的胜负结果通过回调函数返回给使用者,让其自行处理.

/**
 * 游戏结束回调监听
 */
public interface OnGameOverListener{
   void gameOver(boolean isWhiterWinner);
}
/**
 * 设置游戏结束回调监听
 * @param onGameOverListener
 */
public void setOnGameOverListener(OnGameOverListener onGameOverListener){
    this.onGameOverListener = onGameOverListener;
}

到这里看起来我们的五子棋小游戏已经开发完成了,但是一个负责的程序员怎么可能满足于此呢,我们还要让我们的游戏更健壮.

八、防止游戏被回收

我们想象一个场景,当我们正在玩五子棋小游戏,正到关键时刻,来电话了,这时候我们去接电话,这时候我们的小游戏就相当于处于后台,假如这时候手机内存不足,那我们在后台的小游戏就可能被内存回收了,当我们打完电话,发现棋盘上一个棋子都没啦,是不是很伤心,为了解决这个问题,我们就需要重写onSaveInstanceState()方法和onRestoreInstanceState()方法来保存和恢复我们的游戏状态.

我们可以通过旋转屏幕模拟出上面提到的情况,旋转屏幕就会触发上面两个函数.

/**
 * 防止内存不足活动被回收
 */
private static final String INSTANCE = "instance";
private static final String INSTANCE_GAME_OVER = "instance_game_over";
private static final String INSTANCE_WHITE_ARRAY = "instance_white_array";
private static final String INSTANCE_BLACK_ARRAY = "instance_black_array";


@Override
protected Parcelable onSaveInstanceState() {
    Bundle bundle = new Bundle();
    bundle.putParcelable(INSTANCE, super.onSaveInstanceState());
    bundle.putBoolean(INSTANCE_GAME_OVER, isGameOver);
    bundle.putParcelableArrayList(INSTANCE_WHITE_ARRAY, mWhiteArray);
    bundle.putParcelableArrayList(INSTANCE_BLACK_ARRAY, mBlackArray);
    return bundle;
}

@Override
protected void onRestoreInstanceState(Parcelable state) {
    if (state instanceof Bundle){
        Bundle bundle = (Bundle) state;
        isGameOver = bundle.getBoolean(INSTANCE_GAME_OVER);
        mWhiteArray = bundle.getParcelableArrayList(INSTANCE_WHITE_ARRAY);
        mBlackArray = bundle.getParcelableArrayList(INSTANCE_BLACK_ARRAY);
        super.onRestoreInstanceState(bundle.getParcelable(INSTANCE));
        return;
    }
    super.onRestoreInstanceState(state);
}

好了,这样我们的游戏就健壮了不少.

九、再来一局

一个游戏怎么可以只玩一次呢,所以我们这里还需要向使用者保留一个再来一局的方法.

/**
 * 重新开始,再来一局
 */
public void restart(){
    mBlackArray.clear();
    mWhiteArray.clear();
    isGameOver = false;
    isWhiteWinner = false;
    //重绘
    invalidate();
}

十、使用该自定义View

定义已经全部完成了,现在使用就非常简单了.

activity_main.xml文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/main_bg"
    tools:context="com.codekong.wuziqi.activity.MainActivity">

    <com.codekong.wuziqi.view.WuziqiPanel
        android:id="@+id/id_wuziqi_panel"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerInParent="true" />
</RelativeLayout>

MainActivity.java文件

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        WuziqiPanel panel = (WuziqiPanel) findViewById(R.id.id_wuziqi_panel);
        panel.setOnGameOverListener(new WuziqiPanel.OnGameOverListener() {
            @Override
            public void gameOver(boolean isWhiterWinner) {
                //处理胜负结果
            }
        });
    }
}

十一、结语

游戏的介绍就到此为止了,希望可以帮助到需要的人.
源代码已经在Github开源,开源地址: https://github.com/codekongs/WuZiQi
欢迎大家start和fork
下集预告:五子棋小游戏之AI篇,通过算法实现人机对战。

作者:bingjianIT 发表于2016/11/18 21:09:04 原文链接
阅读:31 评论:0 查看评论

Android 图片缓存函数库 Glide vs Picasso

$
0
0

@author ASCE1885的 Github 简书 微博 CSDN 知乎
本文由于潜在的商业目的,不开放全文转载许可,谢谢!

mingwang.png-531.3kB

广而告之时间:我的新书《Android 高级进阶》(https://item.jd.com/10821975932.html在京东开始预售了,欢迎订购!

TB2MnqlXH1J.eBjSszcXXbFzVXa_!!1020536390.png-39kB

本文基于《Introduction to Glide, Image Loader Library for Android, recommended by Google》1和《Glide vs. Picasso》2这两篇文章翻译整理而成。

Glide3 和 Picasso4 都是 Android 世界中非常流行的图片加载函数库,Android 应用开发者在职业生涯中至少都应该用过其中一种。这两个函数库都提供了很多特性,经过优化,图片加载速度非常快,而且都在很多实际项目中通过了测试。从某种程度上面讲,Glide 是 Picasso 的一个变种,表面上看,它们的工作原理是一样的,但实际上,无论在图片的下载,缓存还是图片加载进内存的方式,这两个库都存在很大的区别。本文我们就来对比它们的主要区别,看看哪个库是应用开发中的最佳选择。

本文比较的是 Glide v3.7.0 和 Picasso v2.5.2,当你阅读本文时最新版本可能已经发生了变化。

函数库的导入

Picasso 和 Glide 都托管在 jcenter 中,因此,导入到工程中的方式差不多,Picasso 如下所示:

dependencies {
    compile 'com.squareup.picasso:picasso:2.5.2'
}

Glide 如下所示:

repositories {
    mavenCentral() // jcenter() works as well because it pulls from Maven Central
}

dependencies {
    compile 'com.github.bumptech.glide:glide:3.7.0'
    compile 'com.android.support:support-v4:19.1.0'
}

其中 Glide 依赖于 support-v4,但由于几乎所有项目都会依赖它,所以可以和项目现有的依赖版本共用。

包大小和方法数

从包大小方面看,Glide 差不多是 Picasso 的 3.5 倍,如下图所示:

1-rQe23b0xA6Fpd6zopjx2aQ.png-7.7kB

Picasso 的方法数是 849 个,而 Glide 方法数是 2678 个。这个数字对于 Android DEX 文件的 65535 方法数限制问题来说已经相当大了,在试用过程中一定要记得开启 ProGuard 进行混淆压缩。

1-SE-DjIHhPW8iwf4giJ66aA.png-8.1kB

语法

这两个函数库实现从指定 URL 地址加载图片并显示在 ImageView 上面的语法几乎是一样的,而且都支持渐变动画和基于中间的裁剪(Center Crop)。你当然也可以给 ImageView 设置加载时以及加载失败时的占位图。

Picasso 语法如下:

Picasso.with(context)
    .load(url)
    .centerCrop()
    .placeholder(R.drawable.user_placeholder)
    .error(R.drawable.user_placeholder_error)
    .into(imageView);

Gilde 语法如下:

Glide.with(myFragment)
    .load(url)
    .centerCrop()
    .placeholder(R.drawable.loading_spinner)
    .crossFade()
    .into(myImageView);

Glide 相比 Picasso 的一大优势是它可以和 Activity 以及 Fragment 的生命周期相互协作,我们在调用 Glide.with() 函数时可以将 Activity 或者 Fragment 的实例传进去,这样 Glide 就会自动将图片加载等操作和组件的生命周期例如 onPause()onResume() 关联起来。

with.png-7.2kB

磁盘缓存

这两个函数库都支持从指定 URL 地址将图片下载下来,并缓存到磁盘上,但两者缓存的策略有所不同。

Picasso 将图片下载后会不经压缩直接将图片整个缓存到磁盘中,当需要用到图片时,它会直接返回这张完整大小的图片,并在运行时根据 ImageView 的大小作适配。

Glide 的原理与之不同,它从指定 URL 地址下载图片后会首先根据 ImageView 的大小适配图片,然后将适配后的图片再存储到磁盘中。因此,如果你使用不同大小的 ImageView 加载同一张图片,Glide 将会以不同的分辨率缓存这张图片的两份不同的拷贝。这样可能会增加磁盘缓存的大小,当然好处也是有的,在后面我们会提到。

如前所述,当我们调整 ImageView 的大小时,Picasso 只会缓存完整大小的一张图片,而 Glide 会根据 ImageView 的大小分别缓存对应分辨率的图片。Glide 这种方法的缺点是即使这张图片已经在一个 ImageView 中加载过,如果另一个大小不同的 ImageView 也要加载这张图片,那么这张图片仍然需要重新走一遍流程:到指定的 URL 地址下载,然后根据 ImageView 大小适配,最后缓存到磁盘中。

当然,上面我们说的是默认情况,如果在使用 Glide 时不希望每次都去远程服务器下载完整大小的图片,那么在初始化时可以添加如下 diskCacheStrategy 配置:

Glide.with(myFragment)
    .load(url)
    .diskCacheStrategy(DiskCacheStrategy.ALL)
    .into(myImageView);

这样配置后,Glide 下次从指定 URL 下载图片,会同时在磁盘上缓存完整的图片和经过大小适配后的图片,下次另一个大小不同的 ImageView 也要加载这张图片,Glide 会直接从磁盘获取完整大小的图片并进行大小适配,缓存后显示在这个 ImageView 中。

内存

默认情况下,Glide 将图片加载进内存时使用的是 RGB_565 的配置,而 Picasso 则使用 ARGB_8888 配置。因此,在对比两者内存占用大小时,为了公平起见我们通过继承 GlideModule 类,并将它加载图片时所用的格式由 RGB_565 修改为 ARGB_8888,类似如下代码所示:

public class MyGlideModule implements GlideModule {
    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
        builder.setDecodeFormat(DecodeFormat.ALWAYS_ARGB_8888);
    }

    @Override
    public void registerComponents(Context context, Glide glide) {
        //
    }
}

当然,为了使这个 GlideModule 生效,我们还需要将这个 GlideModule 在 proguard.pro 文件中 keep 住,如下所示:

-keepnames class com.asce1885.MyGlideModule
// 更通用的方式如下:
// -keep public class * implements com.bumptech.glide.module.GlideModule

接着在 AndroidManifest.xml 文件中注册这个 GlideModule,如下所示:

<manifest ...>
    <!-- ... permissions -->
    <application ...>
        <meta-data
            android:name="com.asce1885.MyGlideModule"
            android:value="GlideModule" />
        <!-- ... activities and other components -->
    </application>
</manifest>

下图就是修改后分别使用 Glide 和 Picasso 在线加载图片时的内存占用:

1--TOjFF8NJ6W7bmHZycjDtw.png-12.5kB

可以看到,在加载同样配置的图片时,Glide 内存占用更少,这从前面的讨论中其实可以猜测到了,Picasso 是将完整大小的图片加载进内存,然后依赖 GPU 来根据 ImageView 的大小来适配并渲染图片,而 Glide 是针对每个 ImageView 适配图片大小后再存储到磁盘的,这样加载进内存的是压缩过的图片,内存占用自然就比 Picasso 要少。Glide 这种做法有助于减少 OutOfMemoryError 的出现。

图片加载的耗时

当从指定 URL 地址加载图片时,这两个函数库都会首先检查图片是否已经在缓存中,如果不存在才去 URL 地址中下载。当我们从远程 URL 地址下载图片时,Picasso 相比 Glide 要快很多。可能的原因是 Picasso 下载完图片后直接将整个图片加载进内存,而 Glide 还需要针对每个 ImageView 的大小来适配压缩下载到的图片,这个过程需要耗费一定的时间。(当然我们可以使用 thumbnail() 来减少压缩的时间,后面会讨论到)

1-P7b1K_pp494aPLZ2sFI3Sw.gif-355.2kB

当然,如果直接从磁盘缓存中加载图片的话,Glide 要比 Picasso 快。这要归功于 Glide 的设计。Picasso 在将图片设置到 ImageView 之前,需要在运行时将图片适配压缩到 ImageView 的大小,这会耗费一定的时间,即使我们使用 .noFade() 来取消图片加载时的渐变效果也是如此。

1-0nIGJyOVgut-kCDMbu3kIg.gif-488.3kB

Picasso 和 Glide 的相同特性

文章开头我们说过,Glide 可以看作是 Picasso 的变种,因此我们可以发现,两者在特性和使用方式上有很多共同点,例如图片大小的适配操作:

// Picasso
.resize(300, 200);

// Glide
.override(300, 200);

图片居中裁剪:

// Picasso
.centerCrop();

// Glide
.centerCrop();

图片的变换:

// Picasso
.transform(new CircleTransform())

// Glide
.transform(new CircleTransform(context))

给 ImageView 设置默认图片和出错时显示的图片:

// Picasso
.placeholder(R.drawable.placeholder)
.error(R.drawable.imagenotfound)

// Glide
.placeholder(R.drawable.placeholder)
.error(R.drawable.imagenotfound)

可以看到,如果你之前使用过 Picasso,那么想移植代码到 Glide 是非常容易的。

Glide 独有的特性

  • 对 GIF 动画的支持:Glide 能很好的支持 GIF 动画,加载 GIF 图很简单,直接使用 Glide.with(...).load(...) 即可。而且,由于 Glide 能够和 Activity 的生命周期协作,GIF 图片在 onStop() 生命周期函数调用时会停止动画,从而减少动画后台电量的耗费。目前 Picasso 是不支持 GIF 动画的,因此,如果你的应用想支持 GIF 动画的显示,那么只能选择 Glide。
  • 缩略图的支持:使用 Glide,你可以在同一时间加载多张图片到同一个 ImageView 中,例如可以首先加载只有 ImageView 十分之一大小的缩略图,然后在上面再加载完整大小的图片。

总结

从上面的分析可以看出,Glide 继承自 Picasso,而且青出于蓝而胜于蓝,相信通过上面的对比大家应该已经有了自己的选择。

欢迎关注我的微信公众号 ASCE1885,专注与原创或者分享 Android,iOS,ReactNative,Web 前端移动开发领域高质量文章,主要包括业界最新动态,前沿技术趋势,开源函数库与工具等。

作者:ACE1985 发表于2016/11/18 21:46:23 原文链接
阅读:34 评论:0 查看评论

Android大文件上传秒传之实战篇

$
0
0

源码传送门


在上一篇文章我们介绍了获取大文件的一个唯一的特征值MD5,通过MD5我们可以唯一的标识一个文件,并可以实现秒传效果,今天的这篇文章主要介绍大文件的上传操作,当然谈到上传文件,网络是必不可少的,现在也有很多较为流行的网络框架,如volley,OkHttp,Retrofit。而今天的这篇文章是采用最原始的上传文件的方法,通过HttpClient上传文件的方式。

HttpClient API

在API 23(6.0系统)之前,HttpClient类是Android API中本身自带的方法,但是在23及以后的版本中谷歌放弃了HttpClient,如果想要使用需要在gradle文件中加上下面代码

android {
    useLibrary 'org.apache.http.legacy'
    }

加入上面的代码后,我们build一下就可以API23及以后版本中可以继续使用HttpClient,在使用HttpClient上传文件时可以使用MultipartEntity,FileBody,要使用这个类对象的话,我们需要导入相关jar包,在此我使用的是httpmine-4.1.3.jar。可能有些人说了,为何废弃了,还要用,不要问为什么,因为我也不知道,哈哈,其实是懒,主要是公司老项目用的是这个,还没准备大动,所以就在这基础上做的。当然后期肯定要使用最新最流行的的技术,暂时未考虑(写文章的时候正在学习Retrofit+RxJava,也学的已经差不多了,入了门道,准备开刀)。

Demo运行图

这里写图片描述

文件上传分析

在分析文件分块上传之前我们先来介绍如何直接上传单个文件。在Android中的apache包中有一个HttpClient的默认实现类DefaultHttpClient,在上传的时候我们需要指定上传方式如是GET,POST等请求方式,而在apache包中提供了了对应的HttpPost,HttpGet.在这里我们使用POST请求。如下代码

        MultipartEntity mpEntity=new MultipartEntity();
        try {
            mpEntity.addPart("md5", new StringBody(chunkInfo.getMd5()));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        FileBody fileBody = new FileBody(new File(chunkInfo.getFilePath()));
        mpEntity.addPart("file", fileBody);
        HttpPost post = new HttpPost(actionUrl);
        // 发送请求体
        post.setEntity(mpEntity);
        DefaultHttpClient dhc = new DefaultHttpClient();
        try {
            dhc.getParams().setParameter(
                    CoreConnectionPNames.CONNECTION_TIMEOUT, 10000);
            HttpResponse response = dhc.execute(post);
            int res = response.getStatusLine().getStatusCode();
            Log.e("图片上传返回响应码", res + ",");
            switch (res) {
                case 200:
                    //流形式获得
                    StringBuilder builder = new StringBuilder();
                    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
                    for (String s = bufferedReader.readLine(); s != null; s = bufferedReader.readLine()) {
                        builder.append(s);
                    }
                    retMsg = builder.toString();
                    break;
                case 404:
                    retMsg = "-1";
                    break;
                default:
                    retMsg = "500";
            }

        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

很简单,通过MultipartEntity,FileBody就可以实现文件上传了。上面的代码很简单,当然如果想展示上传进度的话,我们只需要写个类继承FilterOutputStream,就可以自己写个监听回调展示进度,然后再发个广播更新UI,详细代码不贴了,可点击一键直达查看。

在上传整个文件的时候我们看到主要用到的是FileBody,那么我们就可以从这个地方入手,实现文件分块上传。通过源码写文件主要是通过writeTo()方法实现的

    /** @deprecated */
    @Deprecated
    public void writeTo(OutputStream out, int mode) throws IOException {
        this.writeTo(out);
    }

    public void writeTo(OutputStream out) throws IOException {
        if(out == null) {
            throw new IllegalArgumentException("Output stream may not be null");
        } else {
            FileInputStream in = new FileInputStream(this.file);

            try {
                byte[] tmp = new byte[4096];

                int l;
                while((l = in.read(tmp)) != -1) {
                    out.write(tmp, 0, l);
                }

                out.flush();
            } finally {
                in.close();
            }
        }
    }

看到writeTo方法的具体实现后你就知道了,通过while((l = in.read(tmp)) != -1)判断并循环读取文件到输出流。那么既然我们是讲文件分块上传,我们可以读取文件的一部分就可以了这样就可以实现分块上传了。

文件分块分析

对于文件的从指定位置读取指定大小数据,我用了RandomAccessFile对文件随机读取,通过seek()方法指定读取的起始位置
假如我们我们的文件是长度大小fileLength,我们将分块大小是chunkLength.那么我们分块数量计算为

int chunks=(int)(fileLength/chunkLength+(fileLength%chunkLength>0?1:0));

这样我们就计算了分块总数,则我们可以计算我们每一次上传的块的起始位置如下

offset=chunk*chunkLength;//我们服务器将第一块为0块,如果你的服务接口设的是从1开始,那就是offset就为(chunk-1)*chunkLength;

计算出了offset,我们上传每一块只需要执行代码randomAccessFile.seek(chunk*chunkLength);即可,然后读取chunkLength长度的数据。
好了,代码来了

自定义FileBody

/**
 * Created by xiehui on 2016/10/13.
 */
public class CustomFileBody extends AbstractContentBody {
    private File file = null;
    private int chunk = 0;  //第几个分片
    private int chunks = 1;  //总分片数
    private int chunkLength = 1024 * 1024 * 1; //分片大小1MB
    public CustomFileBody(File file) {
        this(file, "application/octet-stream");
    }
    public CustomFileBody(ChunkInfo chunkInfo) {
        this(new File(chunkInfo.getFilePath()), "application/octet-stream");
        this.chunk = chunkInfo.getChunk();
        this.chunks = chunkInfo.getChunks();
        this.file = new File(chunkInfo.getFilePath());
        if (this.chunk == this.chunks) {
            //先不判断,固定1M
            //this.chunkLength=this.file.length()-(this)
        }
    }
    public CustomFileBody(File file, String mimeType) {
        super(mimeType);
        if (file == null) {
            throw new IllegalArgumentException("File may not be null");
        } else {
            this.file = file;
        }
    }
    @Override
    public String getFilename() {
        return this.file.getName();
    }

    @Override
    public String getCharset() {
        return null;
    }

    public InputStream getInputStream() throws IOException {
        return new FileInputStream(this.file);
    }

    @Override
    public String getTransferEncoding() {
        return "binary";
    }

    @Override
    public long getContentLength() {
        return chunkLength;
    }

    @Override
    public void writeTo(OutputStream out) throws IOException {
        if (out == null) {
            throw new IllegalArgumentException("Output stream may not be null");
        } else {
            //不使用FileInputStream
            RandomAccessFile randomAccessFile = new RandomAccessFile(this.file, "r");
            try {
                //int size = 1024 * 1;//1KB缓冲区读取数据
                byte[] tmp = new byte[1024];
                //randomAccessFile.seek(chunk * chunkLength);
                if (chunk+1<chunks){//中间分片
                    randomAccessFile.seek(chunk*chunkLength);
                    int n = 0;
                    long readLength = 0;//记录已读字节数
                    while (readLength <= chunkLength - 1024) {
                        n = randomAccessFile.read(tmp, 0, 1024);
                        readLength += 1024;
                        out.write(tmp, 0, n);
                    }
                    if (readLength <= chunkLength) {
                        n = randomAccessFile.read(tmp, 0, (int)(chunkLength - readLength));
                        out.write(tmp, 0, n);
                    }
                }else{
                    randomAccessFile.seek(chunk*chunkLength);
                    int n = 0;
                    while ((n = randomAccessFile.read(tmp, 0, 1024)) != -1) {
                        out.write(tmp, 0, n);
                    }
                }
                out.flush();
            } finally {
                randomAccessFile.close();
            }
        }
    }

    public File getFile() {
        return this.file;
    }
}

文件分块上传模型类ChunkInfo

 * Created by xiehui on 2016/10/21.
 */
public class ChunkInfo  extends FileInfo implements Serializable{
    /**
     * 文件的当前分片值
     */
    private int chunk=1;
    /**
     * 文件总分片值
     */
    private int chunks=1;
    /**
     * 下载进度值
     */
    private int progress=1;

    public int getChunks() {
        return chunks;
    }

    public void setChunks(int chunks) {
        this.chunks = chunks;
    }

    public int getChunk() {
        return chunk;
    }

    public void setChunk(int chunk) {
        this.chunk = chunk;
    }

    public int getProgress() {
        return progress;
    }

    public void setProgress(int progress) {
        this.progress = progress;
    }

    @Override
    public String toString() {
        return "ChunkInfo{" +
                "chunk=" + chunk +
                ", chunks=" + chunks +
                ", progress=" + progress +
                '}';
    }
}

具体上传实现

 public String uploadFile() {
        String retMsg = "1";
        CustomMultipartEntity mpEntity = new CustomMultipartEntity(
                new CustomMultipartEntity.ProgressListener() {
                    @Override
                    public void transferred(long num) {
                        Intent intent2 = new Intent();
                        ChunkInfo chunkIntent = new ChunkInfo();
                        chunkIntent.setChunks(chunkInfo.getChunks());
                        chunkIntent.setChunk(chunkInfo.getChunk());
                        chunkIntent.setProgress((int) num);
                        intent2.putExtra("chunkIntent", chunkIntent);
                        intent2.setAction("ACTION_UPDATE");
                        context.sendBroadcast(intent2);
                    }
                });
        try {
            mpEntity.addPart("chunk", new StringBody(chunkInfo.getChunk() + ""));
            mpEntity.addPart("chunks", new StringBody(chunkInfo.getChunks() + ""));
             mpEntity.addPart("fileLength", new StringBody(chunkInfo.getFileLength()));
            mpEntity.addPart("md5", new StringBody(chunkInfo.getMd5()));

        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        CustomFileBody customFileBody = new CustomFileBody(chunkInfo);
        mpEntity.addPart("file", customFileBody);
        HttpPost post = new HttpPost(actionUrl);
        // 发送请求体
        post.setEntity(mpEntity);
        DefaultHttpClient dhc = new DefaultHttpClient();
        try {
            dhc.getParams().setParameter(
                    CoreConnectionPNames.CONNECTION_TIMEOUT, 10000);
            HttpResponse response = dhc.execute(post);
            int res = response.getStatusLine().getStatusCode();
            switch (res) {
                case 200:
                    //流形式获得
                    StringBuilder builder = new StringBuilder();
                    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
                    for (String s = bufferedReader.readLine(); s != null; s = bufferedReader.readLine()) {
                        builder.append(s);
                    }
                    retMsg = builder.toString();
                    break;
                case 404:
                    retMsg = "-1";
                    break;
                default:
                    retMsg = "500";
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return retMsg;

    }

到此文件分块上传已基本完毕。那么此时你可能会问秒传的实现在哪了呢?别激动,在前面的分析中我们上传的参数有一个是md5,我们上传文件后将此值保存在数据库,以及图片的url链接,那么当我们上传文件之前先通过这个调用一个接口并上传参数md5,服务接口查询数据库是否有此md5的文件,如果有的话,直接将图片url返回即可,此时就提示用户文件上传成功,如果数据库没有此md5文件,则上传文件。

接口延伸

由于客户端上传的是文件块,当最后一块上传完成后,如果接口是每一分块保存了一个临时文件,则需要对分块的文件进行合并及删除。这个服务器FileChannel进行进行读写,当然也可以使用RandomAccessFile,因为我们上传了文件的总大小,则接口接收到分块文件时直接创建一个文件并调用randomAccessFile.setLength();方法设置长度,之后通过上传的seek方法在指定位置写入数据到文件即可。

到此,本篇文章真的结束了,若文章有不足或者错误的地方,欢迎指正,以防止给其他读者错误引导

作者:xiehuimx 发表于2016/11/18 22:37:35 原文链接
阅读:9 评论:0 查看评论

Unity3D内置Shader私房课(三)Decal贴花

$
0
0

在Unity内建Shader的DefaultResourcesExtra的目录中,有一个很简单却很实用的shader——Decal。这是一个贴花着色器,可以在模型的表面添加一个贴花纹理。

Unity内建Shader下载地址

如图所示:

(程序员的审美也就这样吧……)

我们看看Decal的代码:
Shader "Legacy Shaders/Decal" {
Properties {
	_Color ("Main Color", Color) = (1,1,1,1)
	_MainTex ("Base (RGB)", 2D) = "white" {}
	_DecalTex ("Decal (RGBA)", 2D) = "black" {}
}

SubShader {
	Tags { "RenderType"="Opaque" }
	LOD 250
	
CGPROGRAM
#pragma surface surf Lambert

sampler2D _MainTex;
sampler2D _DecalTex;
fixed4 _Color;

struct Input {
	float2 uv_MainTex;
	float2 uv_DecalTex;
};

void surf (Input IN, inout SurfaceOutput o) {
	fixed4 c = tex2D(_MainTex, IN.uv_MainTex);
	half4 decal = tex2D(_DecalTex, IN.uv_DecalTex);
	c.rgb = lerp (c.rgb, decal.rgb, decal.a);
	c *= _Color;
	o.Albedo = c.rgb;
	o.Alpha = c.a;
}
ENDCG
}

Fallback "Legacy Shaders/Diffuse"
}

这是一个表面着色器,代码非常简单。

唯一要注意的是:
lerp (c.rgb, decal.rgb, decal.a)
实际上就等于:
decal.a*(decal.rgb-c.rgb)+c.rgb
也就等于:
decal.a*decal+(1-decal.a)*c.rgb

也就相当于使用了常规的混合模式(参考Shader山下(十八)混合(Blend)命令
Blend SrcAlpha OneMinusSrcAlpha
(因为是Opaque,所以透明度也就无所谓了)

作者:ecidevilin 发表于2016/11/18 22:53:54 原文链接
阅读:13 评论:0 查看评论

由单例模式的优化,引出的java线程数据同步和类加载顺序知识点总结

$
0
0

由单例模式的优化,引出的java线程数据同步和类加载顺序知识点总结

摘要

几种单例模式的优缺点及其改进优化

DCL失效问题的原因以及解决

java中线程同步关键字final和volatile

java内存屏障

java类加载顺序总结

饿汉单例


    //片段1
    class Singleton1{
        private static Singleton1 instance = new Singleton1();

        private Singleton1(){}

        public static Singleton1 getInstance(){
            return instance;
        }
    }
  1. 线程安全的
  2. 在加载类时就已经对单例进行了初始化,不能做到使用时再进行资源初始化。需要考虑单例对象资源消耗方面的问题和资源加载的时机
  3. 不建议使用

懒汉单例


    //片段2
    class Singleton2{
        private static Singleton2 instance;

        private Singleton2(){}

        public static Singleton2 getInstance(){
            if(instance == null)
                instance = new Singleton2();
            return instance;
        }
    }

    //片段3
    class Singleton3{
        private static Singleton3 instance;

        private Singleton3(){}

        private synchronized static Singleton3 getInstance(){
            if(instance == null)
                instance = new Singleton3();
            return instance;
        }
    }
  1. 在首次调用时才会进行资源的初始化,一定程度上节省了资源
  2. 片段2是非线程安全的,片段3是线程安全的,但是每次获取单例时都会进行同步,造成不必要的同步开销,效率不高
  3. 不建议使用

DCL(double check lock) 单例


    //片段4
    class Singleton4{
        private static Singleton4 instance;

        private Singleton4(){}

        private static Singleton4 getInstance(){
            if(instance == null){
                synchronized(Singleton4.class){
                    if(instance == null)
                        instance = new Singleton4();
                }
            }
            return instance;
        }
    }
  1. 保证只在第一次调用时初始化单例资源,资源利用率高
  2. 线程安全
  3. 在jdk1.5之前,高并发情况下可能会遇到DCL失效的情况

“双重检查”,线程th1和线程th2同时调用getInstance()方法,此时Singleton4未被实例化,th1和th2进入到第一个if判断中,th1率先获取到了同步锁进入到同步代码块中,th2等待获取该类的同步锁,之后Singleton4正常实例化后,th1释放同步锁,获取到单例对象,th2此时获取到了同步锁进入同步代码块中,此时第二个if能够保证不会继续进行资源初始化,保证单例的唯一性

DCL失效问题

instance = new Singleton4();这句代码不是一个原子操作,会被编译为好几条汇编命令,主要做了三件事情:

  1. 给Singelton4的实例分配内存——实例化
  2. 为Singleton4的成员变量进行初始化和调用其构造函数——初始化
  3. 将Singleton4的对象引用指向分配的内存空间(此时的instance就不是null了

但是由于java编译器允许处理器乱序执行,以及jkd1.5之前的JMM(java memory model)中的cache、寄存器到主内存回写顺序的规定,上面第二条和第三条的执行顺序是无法保证的(正确执行顺序应该是1-2-3),即有可能instance不为null了但是还没有被初始化,存在第三条被执行,第二条未被执行的情况下(即1-3-2),被切换到线程B中,此时instance不为null,于是线程B得到了一个资源没有被初始化的单例,在使用时就会出错。

缺乏同步会导致无法实现可见性,这使得确定何时写入对象引用而不是原语值变得更加困难。在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在(更新值位于工作内存还没有同步到主存,而主存此时的值还是旧值)。(这就是造成著名的双重检查锁定(double-checked-locking)问题的根源,其中对象引用在没有同步的情况下进行读操作,产生的问题是您可能会看到一个更新的引用,但是仍然会通过该引用看到不完全构造的对象)。

于是再jdk1.5之后,具体化了volatile关键字,保证每次instance对象每次取用都从主内存中读取,并且禁止编译器优化造成的对指令的重新排序,就可以使用DCL来完成单例模式。

volatile关键字

任何被volatile修饰的变量,会防止编译器“智能”优化代码,读写操作都不会调用工作内存而是直接取主存中重新加载此变量,即保证了内存可见性。因此对于Valatile修饰的变量的修改,所有线程马上就能看到,但是volatile不能保证对变量的修改是有序的。例如片段5代码


    //片段5
    public class VolatileTest
    {   
       public volatile int a;   
       public void  add(int count){   
            a++;   
       }   
    } 

参考关于java自增操作的原子性

自增操作主要是以下几个步骤:
1. 加载局部变量表的变量
2. 将当前栈顶对象的引用赋值一份
3. 获取要进行操作的变量对应id的值,将其值压入栈顶
4. 将int型的值1压入栈顶
5. 将栈顶两个int类型的元素相加,并将其值压入栈顶
6. 将栈顶的值赋值给要进行操作的变量对应的id,即一个写回操作,也被称作“内存屏障”

内存屏障(memory barrier)

内存屏障(memory barrier)是一个CPU指令。基本上,它是这样一条指令: a) 确保一些特定操作执行的顺序; b) 影响一些数据的可见性(可能是某些指令执行后的结果)。编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。

内存屏障(memory barrier)和volatile什么关系?

上面的虚拟机指令里面有提到,如果你的字段是volatile,Java内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令。这意味着如果你对一个volatile字段进行写操作,你必须知道:1、一旦你完成写入,任何访问这个字段的线程将会得到最新的值。2、在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。

同时也需要知道,java中主存和工作内存的区别java线程内存模型,线程、工作内存、主内存

cpu计算读取数据的顺序是:寄存器-高速缓存-内存,计算过程中考虑到数据读取的频繁性,一些数据就被拷贝到寄存器和高速缓存中,在计算完毕后,再将这些数据同步到内存中去。当多个线程同时计算某个内存的数据时,就会遇到多线程并发问题。每个线程都有自己的执行空间,这个执行空间即为工作内存,线程开辟时,工作内存是主存部分数据的拷贝,线程执行的时候用到某变量,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作:读取,修改,赋值等,这些均在工作内存完成,操作完成后再将变量写回主内存。

因此某个线程改变了某个变量的值,在完全计算结束并将数据同步到主存之前,修改的数据只是工作内存中该变量的拷贝,并不是真正的值。

volatile要求程序对变量的每次修改,都写回主内存,这样便对其它线程课件,解决了可见性的问题,但是不能保证数据的一致性;特别注意:原子操作:根据Java规范,对于基本类型的赋值或者返回值操作,是原子操作。但这里的基本数据类型不包括long和double, 因为对于32位的JVM来说,看到的基本存储单位是32位,而long 和double都要用64位来表示。所以无法在一个时钟周期内完成(实际步骤是先写前32位,后写后32位)。对于64位JVM,实现普通long和double的读写不要求是原子的,但是加了volatile关键字的long和double读写操作必须是原子的。知乎中对于64位jvm对long和double读写是否是原子操作的讨论

当一个VolatileTest对象被多个线程共享,a的值不一定是正确的,因为a=a+count包含了好几步操作,而此时多个线程的执行是无序的,因为没有任何机制来保证多个线程的执行有序性和原子性。volatile存在的意义是,任何线程对a的修改,都会马上被其他线程读取到,因为直接操作主存,没有线程对工作内存和主存的同步。所以,volatile的使用场景是有限的,在有限的一些情形下可以使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:

  1. 对变量的写操作不依赖于当前值
  2. 该变量没有包含在具有其他变量的不变式中

所以简单来说,volatile适合这种场景:一个变量被多个线程共享,线程直接给这个变量赋值。


    //片段6
    class Singleton5{
        private volatile static Singleton5 instance = null;

        private Singleton5(){}

        private static Singleton5 getInstance(){
            if(instance == null){
                synchronized(Singleton5.class){
                    if(instance == null)
                        instance = new Singleton5();
                }
            }
            return instance;
        }
    }
进一步优化

直接上代码


    //片段7
    class Singleton6{
        private volatile static Singleton6 instance;

        private Singleton6(){}

        private static Singleton6 getInstance(){
            Singleton6 result = instance;
            if(result == null){
                synchronized(Singleton5.class){
                    result = instance;
                    if(result == null)
                        instance = result= new Singleton6();
                }
            }
            return result;
        }
    }

优化原理

前面说过了,被关键字volatile修饰的对象的读写操作之前都是要求从主存中重新加载的,而cpu从主存读取数据速度要比工作内存中读取数据速度慢,所以在new的过程中,new出的对象赋值给局部变量result再赋值给instance只需要对主存中的instance进行一次写操作,即读-读-写,而不是片段5中的读-读-读写操作。速度要快一点。

单例推荐写法


    //片段8
    class Singleton7{
        private Singleton7(){}

        public Singleton7 getInstance(){
            return Singleton7Holder.instance;
        }

        private static class Singleton7Holder{
            public static final Singleton7 instance = new Singleton7();
        }
    }

利用静态内部类的加载顺序,和关键字final得出以上的结果
1. 在需要的时候进行单例的初始化和资源加载
2. 单例对象的唯一性,线程安全

这一块主要需要两个知识点,一是关键字final的特性,二是java类加载顺序

final关键字

关键字final可以视为 C++ 中const机制的一种受限版本,用于构造不可变对象。final 类型的域是不能修改的(但如果 final 域所引用的对象时可变的,那么这些被引用的对象是可以修改的)。然而,在 Java 内存模型中,final 域还有着特殊的语义。final 域能确保初始化过程的安全性,从而可以不受限制的访问不可变对象,并在共享这些对象时无需同步。
在并发当中,原理是通过禁止cpu的指令集重排序,参考重排序详解1重排序详解2,来提供线程的可见性,来保证对象的安全发布,防止对象引用被其他线程在对象被完全构造完成前拿到并使用。

与前面介绍的锁和volatile相比较,对final域的读和写更像是普通的变量访问。对于final域,编译器和处理器要遵守两个重排序规则:
1. 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
2. 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

类加载顺序

测试代码1

    public class ClassLoaderTest {
           public static void main(String[] args){
             new B();
             new A.C();
           }
        }

    class A{
        private P p1 = new P("A--p1");
        static P p3 = new P("A--p3");
        public A(){
            System.out.println("A()");
        }
        private P p2 =new P("A--p2");
        static{
            new P("A--static");       
        }
        {
            new P("A{...}");
        }
        public static class C {
            private P p1 = new P("C--p1");
            static P p3 = new P("C--p3");
            public C(){
                System.out.println("C()");
            }
            private P p2 =new P("C--p2");
            static{
               new P("C--static");  
            }
            {
                new P("C{...}");
            }
        }
    }

    class B extends A {

        static {
            new P("B -- static");
        }

        private P p1 = new P("B --p1");
        static P p3 = new P("B -- p3");
        public B() {
          System.out.println("B()"); 
        }
        public P p2 = new P("B -- p2");

        {new P("B{...}");}
    }

    class P {
        public P(String s) {
          System.out.println(s);
        } 
    }
运行结果1

A–p3

A–static

B – static

B – p3

A–p1

A–p2

A{…}

A()

B –p1

B – p2

B{…}

B()

C–p3

C–static

C–p1

C–p2

C{…}

C()

测试代码2

    public class ClassLoaderTest {
           public static void main(String[] args){
             new B();
             new A.C();
           }
        }

    class A{
        //静态初始化块提前
        static{
            new P("A--static");       
        }
        private P p1 = new P("A--p1");
        //非静态初始化块提前
        {
            new P("A{...}");
        }
        static P p3 = new P("A--p3");
        public A(){
            System.out.println("A()");
        }
        private P p2 =new P("A--p2");

        public static class C {
            private P p1 = new P("C--p1");
            static P p3 = new P("C--p3");
            public C(){
                System.out.println("C()");
            }
            private P p2 =new P("C--p2");
            static{
               new P("C--static");  
            }
            {
                new P("C{...}");
            }
        }
    }

    class B extends A {
        private P p1 = new P("B --p1");
        static P p3 = new P("B -- p3");
        public B() {
          System.out.println("B()"); 
        }
        public P p2 = new P("B -- p2");
        //静态初始化块置后
        static {
            new P("B -- static");
        }
        {new P("B{...}");}
    }

    class P {
        public P(String s) {
          System.out.println(s);
        } 
    }

运行结果2

A–static

A–p3

B – p3

B – static

A–p1

A{…}

A–p2

A()

B –p1

B – p2

B{…}

B()

C–p3

C–static

C–p1

C–p2

C{…}

C()

结论
  1. 加载父类
    1. 为静态属性分配存储空间并初始化赋值
    2. 执行静态初始化块和静态初始化语句(按照代码顺序执行)
  2. 加载子类
    1. 为静态属性分配存储空间并初始化赋值
    2. 执行静态初始化块和静态初始化语句(按照代码顺序执行)
  3. 加载父类构造器
    1. 为实例属性分配存储空间并赋初始化值
    2. 执行实例初始化块和实例初始化语句(按照代码顺序执行)
    3. 执行构造函数
  4. 加载子类构造器
    1. 为实例属性分配存储空间并赋初始化值
    2. 执行实例初始化块和实例初始化语句(按照代码顺序执行)
    3. 执行构造函数
  5. 回到main()函数
  6. 静态内部类在被调用时执行初始化
  7. 内部类的加载过程同上
作者:u012123160 发表于2016/11/18 22:57:03 原文链接
阅读:7 评论:0 查看评论

关于boot.img和recovery.img的修改和编辑

$
0
0
        关于boot.img和recovery.img的编辑和修改方面的文章,希望能够为感兴趣的朋友节约一些看资料的时间。感谢本文的作者:Alansj, DarkriftX, RyeBrye, Will, Try OP9, Tonyb486, Timmmm, Lxrose还有好多不知名的作者们在wiki上的不懈努力。

如何解包/编辑/大包boot.img文件

很多人用自己的方式解决了boot.img的解包/编辑/打包的问题,有人要求我来写一篇关于boot和recovery映像的文件结构和如何对其编辑的文章,于是就有了下面这篇文章。

目录
1、背景知识
2、boot和recovery映像的文件结构
3、对映像文件进行解包、编辑、打包的常规方法
3.1、另一种解包、编辑、打包的方法
4、将新的映像刷回到手机
5、解包、编辑、打包为我们带来了什么
6、本文讲的内容与使用update.zip刷机包不是一码事

正文

1、背景知识

Android手机的文件系统有许多存储器组成,以下是在adb shell下面的输出:
#cat /proc/mtd
dev:    size   erasesize  name
mtd0: 00040000 00020000 "misc"
mtd1: 00500000 00020000 "recovery"
mtd2: 00280000 00020000 "boot"
mtd3: 04380000 00020000 "system"
mtd4: 04380000 00020000 "cache"
mtd5: 04ac0000 00020000 "userdata"

注意,不同的手机在上述存储设备的顺序可能会各不相同!一定要检查您的手机,确定在以下的操作中选择正确的设备号(mtdX,这个X的序号一定要检查清楚)。
在本向导中,我们主要描述对"recovery"和"boot"的存储设备进行操作;"system"存储设备保存了android系统目录的所有数据(在系统启动后会挂载到“system/”目录);“userdata”存储设备将保存了android数据目录中的所有数据(在系统启动后会挂载到“data/”目录,里面是会有很多应用数据以及用户的preference之类的配置数据)。

从上面的输出可以看出来,recovery和boot分区对应着/dev/mtd/mtd1和/dev/mtd/mtd2,在你您开始做任何修改之前一定要做两件事情,第一件事情,一定要先对这两个分区进行备份。

可以使用如下命令进行备份:
# cat /dev/mtd/mtd1 > /sdcard/recovery.img
# cat /dev/mtd/mtd2 > /sdcard/boot.img
(注意added by lxros,只有手机获取了ROOT权限以后才能够执行上述的备份命令)

第二件事情,你您应该把你您最喜欢的update.zip刷机包放置到你您的sd卡的根目录上面。如此一来,即使你您在后续的操作中出了问题,也可以启动到recovery模式进行恢复。

另外一个你您需要知道的重要文件是在android系统目录下的/system/recovery.img,此文件是mtd1存储设备的完全拷贝。这个文件在每次关机的时候,会自动地被写回到mtd1存储设备里面。

这会意味着两个事情:
(1)任何对/dev/mtd/mtd1中数据的直接修改都会在下一次重启手机以后消失。

(2)如果希望对/dev/mtd/mtd1进行修改,最简单的做法是用你您自己的recovery.img替换掉/system/recovery.img。当你您创建自己的update.zip刷机包的时候(特别是在做刷机包的适配的时候),如果你您忘记替换这个/system/recovery.img,这个recovery.img就会在关机的时候被烧写到mtd1里面去或许会变砖。一定要注意这一点!
(译者的话,关于这个/system/recovery.img文件,在2.1的android的平台里面并没有找到,或许这个机制已经out了?!或者偶本人对这段话的理解不够深入?!希望明白的朋友不吝斧正)

2、boot和recovery映像的文件结构

boot和recovery映像并不是一个完整的文件系统,它们是一种android自定义的文件格式,该格式包括了2K的文件头,后面紧跟着是用gzip压缩过的内核,再后面是一个ramdisk内存盘,然后紧跟着第二阶段的载入器程序(这个载入器程序是可选的,在某些映像中或许没有这部分)。此类文件的定义可以从源代码android-src/system/core/mkbootimg找到一个叫做bootimg.h的文件。

(译者的话,原文是一个叫做mkbootimg.h的文件,但从Android 2.1的代码来看,该文件名应该是改为bootimg.h了)。
/*
** +-----------------+
** | boot header     | 1 page
** +-----------------+
** | kernel          | n pages  
** +-----------------+
** | ramdisk         | m pages  
** +-----------------+
** | second stage    | o pages
** +-----------------+
**
** n = (kernel_size + page_size - 1) / page_size
** m = (ramdisk_size + page_size - 1) / page_size
** o = (second_size + page_size - 1) / page_size
**
** 0. all entities are page_size aligned in flash
** 1. kernel and ramdisk are required (size != 0)
** 2. second is optional (second_size == 0 -> no second)
** 3. load each element (kernel, ramdisk, second) at
**    the specified physical address (kernel_addr, etc)
** 4. prepare tags at tag_addr.  kernel_args[] is
**    appended to the kernel commandline in the tags.
** 5. r0 = 0, r1 = MACHINE_TYPE, r2 = tags_addr
** 6. if second_size != 0: jump to second_addr
**    else: jump to kernel_addr
*/
ramdisk映像是一个最基础的小型文件系统,它包括了初始化系统所需要的全部核心文件,例如:初始化init进程以及init.rc(可以用于设置很多系统的参数)等文件。如果你您希望了解更多关于此文件的信息可以参考以下网址:
http://Git.source.android.com/?p=kernel/common.git;a=blob;f=Documentation/filesystems/ramfs-rootfs-initramfs.txt
以下是一个典型的ramdisk中包含的文件列表:
./init.trout.rc
./default.prop
./proc
./dev
./init.rc
./init
./sys
./init.goldfish.rc
./sbin
./sbin/adbd
./system
./data
recovery映像包含了一些额外的文件,例如一个叫做recovery的二进制程序,以及一些对该程序支持性的资源图片文件(当你您按下home+power组合键的时候就会运行这个recovery程序)。
典型的文件列表如下:
./res
./res/images
./res/images/progress_bar_empty_left_round.bmp
./res/images/icon_firmware_install.bmp
./res/images/indeterminate3.bmp
./res/images/progress_bar_fill.bmp
./res/images/progress_bar_left_round.bmp
./res/images/icon_error.bmp
./res/images/indeterminate1.bmp
./res/images/progress_bar_empty_right_round.bmp
./res/images/icon_firmware_error.bmp
./res/images/progress_bar_right_round.bmp
./res/images/indeterminate4.bmp
./res/images/indeterminate5.bmp
./res/images/indeterminate6.bmp
./res/images/progress_bar_empty.bmp
./res/images/indeterminate2.bmp
./res/images/icon_unpacking.bmp
./res/images/icon_installing.bmp
./sbin/recovery

3、对映像文件进行解包、编辑、打包的常规方法

(注意,下面我给你您介绍的是手工命令行方式进行解包以及重新打包的方法,但是我仍然创建了两个perl脚本,这两个脚本可以让你您的解包和打包工作变得轻松许多。可以参考本文的附件unpack-bootimg.zip和repack-bootimg.zip)

如果你您很擅长使用16进制编辑器的话,你您可以打开boot.img或者recovery.img,然后跳过开始的2K的头数据,然后寻找一大堆0的数据,在这一堆0的数据后面,紧跟着1F 8B这两个数字(1F 8B是gzip格式的文件的结束标记)。从此文件开始的地方(跳过2K的头),一大堆0后面紧跟着到1F 8B这两个数字为止的全部数据,就是gzip压缩过的
Linux内核。从1F 8B后面紧跟着的数据一直到文件的结尾包含的全部数据,就是ramdisk内存盘的数据。你您可以把把内核和ramdisk两个文件分别保存下来,在进行分别的修改和处理。我们可以通过un-cpio和un-gzip操作来读取ramdisk文件中的数据,可以使用如下的命令来实现这个目的,以下操作会生成一个目录,直接cd进去就可以看到ramdisk中的数据了:
gunzip -c ../your-ramdisk-file | cpio -i
此命令可以将ramdisk中的所有的文件解包到当前的工作目录下面,然后就可以对它进行编辑了。

当需要重新打包ramdisk的时候,就需要re-cpio然后re-gzip这些数据和目录,可以通过如下命令来实现:(cpio会把所有当前目录下面的文件都打包进去,因此,在进行此步骤之前,请把不需要的文件都清除掉。)
find . | cpio -o -H newc | gzip > ../newramdisk.cpio.gz
最后一步就是通过mkbootimg这个工具,把kernel和ramdisk打包在一起,生成一个boot.img:
mkbootimg --cmdline 'no_console_suspend=1 console=null' --kernel your-kernel-file --ramdisk newramdisk.cpio.gz -o mynewimage.img
这里的mkbootimg工具会在编译android的源代码的时候会在~/android-src/out/host/linux-x86/bin目录下面自动生成。
下载地址:
http://git.source.android.com/?p=platform/system/core.git;a=tree;f=mkbootimg

现在,如果不想背这些复杂的命令或者摆弄那个让人眩晕的16进制编辑器的话,可以尝试使用我编写的用于解包和打包的perl脚本了。希望这些脚本能够节约各位的键盘。

3.1、另一种解包、编辑、打包的方法

下载split_bootimg.zip文件(译者注,会在本文的附件中提供),在此zip文件中包含一个perl文件,split_bootimg.pl脚本,该脚本可以读取boot.img头(根据Android源码中的bootimg.h读取)将kernel和ramdisk读取出来,此脚本也会输出内核命令行和板子名字。
(注意,不要使用从/dev/mtd/mtd2直接拷贝出来的boot.img,此映像可能在读取过程遭到损坏。)
下面是一个从TC4-RC28更新中提取出来的boot.img进行解包操作:
% ./split_bootimg.pl boot.img
Page size: 2048 (0x00000800)
Kernel size: 1388548 (0x00153004)
Ramdisk size: 141518 (0x000228ce)
Second size: 0 (0x00000000)
Board name:
Command line: no_console_suspend=1
Writing boot.img-kernel ... complete.
Writing boot.img-ramdisk.gz ... complete.
解包ramdisk的命令如下:
% mkdir ramdisk
% cd ramdisk
% gzip -dc ../boot.img-ramdisk.gz | cpio -i
% cd ..
解码完毕后,就可以修改了(例如,在default.prop设置ro.secure=0等等)

使用mkbootfs工具(mkbootfs工具是编译完毕Android源代码以后,就会在~/android-src/out/host/linux-x86/bin自动生成)来重新创建ramdisk,可以使用如下命令来操作:
% mkbootfs ./ramdisk | gzip > ramdisk-new.gz
使用mkbootimg来重新创建boot.img,mkbootimg也可以在~/android-src/out/host/linux-x86/bin目录中可以找到:
% mkbootimg --cmdline 'no_console_suspend=1 console=null' --kernel boot.img-kernel --ramdisk ramdisk-new.gz -o boot-new.img
(注意:console=null的命令行选现是从TC4-RC30的boot.img引入的,用以去掉root shell)

4、将新的映像刷回到手机

可以将recovery.img拷贝到/system目录下面,然后重新启动手机,让手机自动为你您刷写到mtd里面(工作原理在上面已经提过了)。对于boot.img可以通过将其拷贝到sd卡的根目录,然后通过手机内的刷写工具将此映像写入到手机中。

例如,使用adb工具将boot.img拷贝到手机的sd卡的根目录:
adb push ./mynewimage.img /sdcard
然后通过adb shell登录手机的shell交互模式,利用命令行进行交互:
# cat /dev/zero > /dev/mtd/mtd2
   write: No space left on device [this is ok, you can ignore]
# flash_image boot /sdcard/mynewimage.img
然后重启。
如果能够正常启动,那么祝贺你您,你您的修改和替换已经成功了;如果不能够顺利启动,则需要重新启动进入recovery模式,并且使用update.zip来恢复。

5、解包、编辑、打包为我们带来了什么

可以修改开机启动时候的画面


作者:u011467537 发表于2016/11/21 19:44:54 原文链接
阅读:58 评论:0 查看评论

Android makefile讲解剖析

$
0
0

        对于一个程序新手而言,好的IDE是他们追捧的对象。但当他接触的代码多了之后,就会逐渐发现IDE不够用了,因为有好多东西用IDE是不好做的,
例如自动编译,测试,版本控制,编译定制等。这跟政治课上的一句话有点像:资本主义开始的时候是促进生产力发展的,但到了后来又成了阻碍生产力发展的因素
了。如果一个程序不能摆脱IDE的限制(不是不用,而是要有选择的用),那么他就很难提高。要知道,IDE和makefile代表了两种不同的思
想:IDE根据强调的是简化计算机用户的交互;而makefile体现的是自动化。
对于一个一开始就接触linux的人来说,makefile可能是比较容易学的(熟能生巧),对于一个一开始就接触Windows的人来
说,makefile就不太好学,这主要是应该很多时候会不自觉地去用Visual Studio(Visual
Studio是个好东西,特别是它的调试)。不知道大叫有没有这个的感觉:一个人如果先接触c,再接触java会比较容易点;如果一个人先接触java,
再接触c,就会比较反感c。
这个先引用一下百度百科对makefile的一些描述:
一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件
需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为 makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。
makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefile都成为了一种在工程方面的编译方法。 
Make工具最主要也是最基本的功能就是通过makefile文件来描述源程序之间的相互关系并自动维护编译工作。
而makefile
文件需要按照某种语法进行编写,文件中需要说明如何编译各个源文件并连接生成可执行文件,并要求定义源文件之间的依赖关系。makefile
文件是许多编译器--包括 Windows NT 下的编译器--维护编译信息的常用方法,只是在集成开发环境中,用户通过友好的界面修改
makefile 文件而已。
对于android而言,android使用的是GNU的make,因此它的makefile格式也是GNU的makefile格式。现在网络上关
于makefile最好的文档就是陈皓的《跟我一起写makefile》,这份文档对makefile进行了详细的介绍,因此推荐大家先看这份文档(电子
版可以到
http://download.csdn.net/detail/andy_android/3783424

下载。

首先我们来看看android里makefile的写法


(1)Android.mk文件首先需要指定LOCAL_PATH变量,用于查找源文件。由于一般情况下
Android.mk和需要编译的源文件在同一目录下,所以定义成如下形式:
LOCAL_PATH:=$(call my-dir)
上面的语句的意思是将LOCAL_PATH变量定义成本文件所在目录路径。

(2)Android.mk中可以定义多个编译模块,每个编译模块都是以include $(CLEAR_VARS)开始
以include $(BUILD_XXX)结束
include $(CLEAR_VARS)
CLEAR_VARS由编译系统提供,指定让GNU MAKEFILE为你清除除LOCAL_PATH以外的所有LOCAL_XXX变量,
如LOCAL_MODULE,LOCAL_SRC_FILES,LOCAL_SHARED_LIBRARIES,LOCAL_STATIC_LIBRARIES等。
include $(BUILD_STATIC_LIBRARY)表示编译成静态库
include $(BUILD_SHARED_LIBRARY)表示编译成动态库。
include $(BUILD_EXECUTABLE)表示编译成可执行程序

(3)举例如下(frameworks/base/libs/audioflinger/Android.mk):
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)  模块一
ifeq ($(AUDIO_POLICY_TEST),true)
  ENABLE_AUDIO_DUMP := true
endif
LOCAL_SRC_FILES:= \
    AudioHardwareGeneric.cpp \
    AudioHardwareStub.cpp \
    AudioHardwareInterface.cpp
ifeq ($(ENABLE_AUDIO_DUMP),true)
  LOCAL_SRC_FILES += AudioDumpInterface.cpp
  LOCAL_CFLAGS += -DENABLE_AUDIO_DUMP
endif
LOCAL_SHARED_LIBRARIES := \
    libcutils \
    libutils \
    libbinder \
    libmedia \
    libhardware_legacy
ifeq ($(strip $(BOARD_USES_GENERIC_AUDIO)),true)
  LOCAL_CFLAGS += -DGENERIC_AUDIO
endif
LOCAL_MODULE:= libaudiointerface
ifeq ($(BOARD_HAVE_BLUETOOTH),true)
  LOCAL_SRC_FILES += A2dpAudioInterface.cpp
  LOCAL_SHARED_LIBRARIES += liba2dp
  LOCAL_CFLAGS += -DWITH_BLUETOOTH -DWITH_A2DP
  LOCAL_C_INCLUDES += $(call include-path-for, bluez)
endif
include $(BUILD_STATIC_LIBRARY)  模块一编译成静态库
include $(CLEAR_VARS)  模块二
LOCAL_SRC_FILES:=               \
    AudioPolicyManagerBase.cpp
LOCAL_SHARED_LIBRARIES := \
    libcutils \
    libutils \
    libmedia
ifeq ($(TARGET_SIMULATOR),true)
 LOCAL_LDLIBS += -ldl
else
 LOCAL_SHARED_LIBRARIES += libdl
endif
LOCAL_MODULE:= libaudiopolicybase
ifeq ($(BOARD_HAVE_BLUETOOTH),true)
  LOCAL_CFLAGS += -DWITH_A2DP
endif
ifeq ($(AUDIO_POLICY_TEST),true)
  LOCAL_CFLAGS += -DAUDIO_POLICY_TEST
endif
include $(BUILD_STATIC_LIBRARY) 模块二编译成静态库
include $(CLEAR_VARS) 模块三
LOCAL_SRC_FILES:=               \
    AudioFlinger.cpp            \
    AudioMixer.cpp.arm          \
    AudioResampler.cpp.arm      \
    AudioResamplerSinc.cpp.arm  \
    AudioResamplerCubic.cpp.arm \
    AudioPolicyService.cpp
LOCAL_SHARED_LIBRARIES := \
    libcutils \
    libutils \
    libbinder \
    libmedia \
    libhardware_legacy
ifeq ($(strip $(BOARD_USES_GENERIC_AUDIO)),true)
  LOCAL_STATIC_LIBRARIES += libaudiointerface libaudiopolicybase
  LOCAL_CFLAGS += -DGENERIC_AUDIO
else
  LOCAL_SHARED_LIBRARIES += libaudio libaudiopolicy
endif
ifeq ($(TARGET_SIMULATOR),true)
 LOCAL_LDLIBS += -ldl
else
 LOCAL_SHARED_LIBRARIES += libdl
endif
LOCAL_MODULE:= libaudioflinger
ifeq ($(BOARD_HAVE_BLUETOOTH),true)
  LOCAL_CFLAGS += -DWITH_BLUETOOTH -DWITH_A2DP
  LOCAL_SHARED_LIBRARIES += liba2dp
endif
ifeq ($(AUDIO_POLICY_TEST),true)
  LOCAL_CFLAGS += -DAUDIO_POLICY_TEST
endif
ifeq ($(TARGET_SIMULATOR),true)
    ifeq ($(HOST_OS),linux)
        LOCAL_LDLIBS += -lrt -lpthread
    endif
endif
ifeq ($(BOARD_USE_LVMX),true)
    LOCAL_CFLAGS += -DLVMX
    LOCAL_C_INCLUDES += vendor/nxp
    LOCAL_STATIC_LIBRARIES += liblifevibes
    LOCAL_SHARED_LIBRARIES += liblvmxservice
#    LOCAL_SHARED_LIBRARIES += liblvmxipc
endif
include $(BUILD_SHARED_LIBRARY) 模块三编译成动态库


(4)编译一个应用程序(APK)
  LOCAL_PATH := $(call my-dir)
  include $(CLEAR_VARS)
   
  # Build all java files in the java subdirectory-->直译(建立在java子目录中的所有Java文件
  LOCAL_SRC_FILES := $(call all-subdir-java-files)
   
  # Name of the APK to build-->直译(创建APK的名称
  LOCAL_PACKAGE_NAME := LocalPackage
   
  # Tell it to build an APK-->直译(告诉它来建立一个APK
  include $(BUILD_PACKAGE)

(5)编译一个依赖于静态Java库(static.jar)的应用程序
  LOCAL_PATH := $(call my-dir)
  include $(CLEAR_VARS)
   
  # List of static libraries to include in the package
  LOCAL_STATIC_JAVA_LIBRARIES := static-library
   
  # Build all java files in the java subdirectory
  LOCAL_SRC_FILES := $(call all-subdir-java-files)
   
  # Name of the APK to build
  LOCAL_PACKAGE_NAME := LocalPackage
   
  # Tell it to build an APK
  include $(BUILD_PACKAGE)

(6)编译一个需要用平台的key签名的应用程序
  LOCAL_PATH := $(call my-dir)
  include $(CLEAR_VARS)
   
  # Build all java files in the java subdirectory
  LOCAL_SRC_FILES := $(call all-subdir-java-files)
   
  # Name of the APK to build
  LOCAL_PACKAGE_NAME := LocalPackage
   
  LOCAL_CERTIFICATE := platform
   
  # Tell it to build an APK
  include $(BUILD_PACKAGE)


(7)编译一个需要用特定key前面的应用程序
  LOCAL_PATH := $(call my-dir)
  include $(CLEAR_VARS)
   
  # Build all java files in the java subdirectory
  LOCAL_SRC_FILES := $(call all-subdir-java-files)
   
  # Name of the APK to build
  LOCAL_PACKAGE_NAME := LocalPackage
   
  LOCAL_CERTIFICATE := vendor/example/certs/app
   
  # Tell it to build an APK
  include $(BUILD_PACKAGE)

(8)添加一个预编译应用程序
  LOCAL_PATH := $(call my-dir)
  include $(CLEAR_VARS)
   
  # Module name should match apk name to be installed.
  LOCAL_MODULE := LocalModuleName
  LOCAL_SRC_FILES := $(LOCAL_MODULE).apk
  LOCAL_MODULE_CLASS := APPS
  LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX)
   
  include $(BUILD_PREBUILT)

(9)添加一个静态JAVA库
  LOCAL_PATH := $(call my-dir)
  include $(CLEAR_VARS)
   
  # Build all java files in the java subdirectory
  LOCAL_SRC_FILES := $(call all-subdir-java-files)
   
  # Any libraries that this library depends on
  LOCAL_JAVA_LIBRARIES := android.test.runner
   
  # The name of the jar file to create
  LOCAL_MODULE := sample
   
  # Build a static jar file.
  include $(BUILD_STATIC_JAVA_LIBRARY)

(10)Android.mk的编译模块中间可以定义相关的编译内容,也就是指定相关的变量如下:
LOCAL_AAPT_FLAGS

LOCAL_ACP_UNAVAILABLE 

LOCAL_ADDITIONAL_JAVA_DIR 
 
LOCAL_AIDL_INCLUDES 

LOCAL_ALLOW_UNDEFINED_SYMBOLS 

LOCAL_ARM_MODE 

LOCAL_ASFLAGS 

LOCAL_ASSET_DIR 

LOCAL_ASSET_FILES 在Android.mk文件中编译应用程序(BUILD_PACKAGE)时设置此变量,表示资源文件,
                  通常会定义成LOCAL_ASSET_FILES += $(call find-subdir-assets)
 
LOCAL_BUILT_MODULE_STEM  
LOCAL_C_INCLUDES 额外的C/C++编译头文件路径,用LOCAL_PATH表示本文件所在目录
                 举例如下:
                 LOCAL_C_INCLUDES += extlibs/zlib-1.2.3
                 LOCAL_C_INCLUDES += $(LOCAL_PATH)/src 
 
LOCAL_CC 指定C编译器

LOCAL_CERTIFICATE  签名认证

LOCAL_CFLAGS 为C/C++编译器定义额外的标志(如宏定义),举例:LOCAL_CFLAGS += -DLIBUTILS_NATIVE=1
 
LOCAL_CLASSPATH 

LOCAL_COMPRESS_MODULE_SYMBOLS 

LOCAL_COPY_HEADERS install应用程序时需要复制的头文件,必须同时定义LOCAL_COPY_HEADERS_TO
 
LOCAL_COPY_HEADERS_TO install应用程序时复制头文件的目的路径

LOCAL_CPP_EXTENSION 如果你的C++文件不是以cpp为文件后缀,你可以通过LOCAL_CPP_EXTENSION指定C++文件后缀名 
                    如:LOCAL_CPP_EXTENSION := .cc
                    注意统一模块中C++文件后缀必须保持一致。

LOCAL_CPPFLAGS 传递额外的标志给C++编译器,如:LOCAL_CPPFLAGS += -ffriend-injection

LOCAL_CXX 指定C++编译器
 
LOCAL_DX_FLAGS

LOCAL_EXPORT_PACKAGE_RESOURCES

LOCAL_FORCE_STATIC_EXECUTABLE 如果编译的可执行程序要进行静态链接(执行时不依赖于任何动态库),则设置LOCAL_FORCE_STATIC_EXECUTABLE:=true 
                              目前只有libc有静态库形式,这个只有文件系统中/sbin目录下的应用程序会用到,这个目录下的应用程序在运行时通常
                              文件系统的其它部分还没有加载,所以必须进行静态链接。
 
LOCAL_GENERATED_SOURCES
 
LOCAL_INSTRUMENTATION_FOR

LOCAL_INSTRUMENTATION_FOR_PACKAGE_NAME

LOCAL_INTERMEDIATE_SOURCES

LOCAL_INTERMEDIATE_TARGETS

LOCAL_IS_HOST_MODULE

LOCAL_JAR_MANIFEST

LOCAL_JARJAR_RULES

LOCAL_JAVA_LIBRARIES 编译java应用程序和库的时候指定包含的java类库,目前有core和framework两种
                     多数情况下定义成:LOCAL_JAVA_LIBRARIES := core framework
                     注意LOCAL_JAVA_LIBRARIES不是必须的,而且编译APK时不允许定义(系统会自动添加)
 
LOCAL_JAVA_RESOURCE_DIRS 

LOCAL_JAVA_RESOURCE_FILES 

LOCAL_JNI_SHARED_LIBRARIES 

LOCAL_LDFLAGS 传递额外的参数给连接器(务必注意参数的顺序)
 
LOCAL_LDLIBS 为可执行程序或者库的编译指定额外的库,指定库以"-lxxx"格式,举例:
             LOCAL_LDLIBS += -lcurses -lpthread
             LOCAL_LDLIBS += -Wl,-z,origin 
 
LOCAL_MODULE 生成的模块的名称(注意应用程序名称用LOCAL_PACKAGE_NAME而不是LOCAL_MODULE)

LOCAL_MODULE_PATH 生成模块的路径
 
LOCAL_MODULE_STEM 
 
LOCAL_MODULE_TAGS 生成模块的标记 
 
LOCAL_NO_DEFAULT_COMPILER_FLAGS 

LOCAL_NO_EMMA_COMPILE 

LOCAL_NO_EMMA_INSTRUMENT 

LOCAL_NO_STANDARD_LIBRARIES 

LOCAL_OVERRIDES_PACKAGES 

LOCAL_PACKAGE_NAME APK应用程序的名称 

LOCAL_POST_PROCESS_COMMAND
 
LOCAL_PREBUILT_EXECUTABLES 预编译including $(BUILD_PREBUILT)或者$(BUILD_HOST_PREBUILT)时所用,指定需要复制的可执行文件

LOCAL_PREBUILT_JAVA_LIBRARIES 

LOCAL_PREBUILT_LIBS 预编译including $(BUILD_PREBUILT)或者$(BUILD_HOST_PREBUILT)时所用, 指定需要复制的库.

LOCAL_PREBUILT_OBJ_FILES 

LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES  
 
LOCAL_PRELINK_MODULE 是否需要预连接处理(默认需要,用来做动态库优化)

LOCAL_REQUIRED_MODULES 指定模块运行所依赖的模块(模块安装时将会同步安装它所依赖的模块)
 
LOCAL_RESOURCE_DIR

LOCAL_SDK_VERSION

LOCAL_SHARED_LIBRARIES 可链接动态库
 
LOCAL_SRC_FILES 编译源文件


LOCAL_STATIC_JAVA_LIBRARIES 

LOCAL_STATIC_LIBRARIES 可链接静态库 
 
LOCAL_UNINSTALLABLE_MODULE 

LOCAL_UNSTRIPPED_PATH
 
LOCAL_WHOLE_STATIC_LIBRARIES 指定模块所需要载入的完整静态库(这些精通库在链接是不允许链接器删除其中无用的代码)
 
LOCAL_YACCFLAGS
 
OVERRIDE_BUILT_MODULE_PATH

接下来我们详细看一下android里的makefile文件


android最顶层的目录结构如下:
.   
|-- Makefile        (全局的Makefile)    
|-- bionic          (Bionic含义为仿生,这里面是一些基础的库的源代码)    
|-- bootloader      (引导加载器)    
|-- build           (build目录中的内容不是目标所用的代码,而是编译和配置所需要的脚本和工具)    
|-- dalvik          (JAVA虚拟机)    
|-- development     (程序开发所需要的模板和工具)    
|-- external        (目标机器使用的一些库)    
|-- frameworks      (应用程序的框架层)    
|-- hardware        (与硬件相关的库)    
|-- kernel          (Linux2.6的源代码)    
|-- packages        (Android的各种应用程序)    
|-- prebuilt        (Android在各种平台下编译的预置脚本)    
|-- recovery        (与目标的恢复功能相关)    
`-- system          (Android的底层的一些库)
本文将要分析的是build目录下的makefile和shell文件,android的代码是1.5的版本。
主要的目录结构如下:
1.makefile入门
    1.1 makefile helloworld
    1.2 用makefile构建交叉编译环境
    1.3 makefile里面的一些技巧
2.android makefile分析
    2.1 android shell分析
    2.2 android build下的各个makefile分析
3. android其他目录的android.mk分析

大家先通过网络的一些文章来了解一下andoroid的makefile。
1.
Android build system
2.
Android Building System 分析
3.
Android Build System(介绍使用)
1.1 makefile helloworld
  
Makefile的规则如下:
  
target ... : prerequisites ... 
  
command ... ...
  
target可以是一个目标文件,也可以是Object File(例如helloworld.obj),也可以是执行文件和标签。
  
prerequisites就是生成target所需要的文件或是目标。
  
command
也就是要达到target这个目标所需要执行的命令。这里没有说“使用生成target所需要执行的命令”,是因为target可能是标签。需要注意的是
command前面必须是TAB键,而不是空格,因此喜欢在编辑器里面将TAB键用空格替换的人需要特别小心了。
  
我们写程序一般喜欢写helloworld,当我们写了一个c的helloworld之后,我们该如何写helloworld来编译helloworld.c呢?
  
下面就是编译helloworld的makefile。
                       
helloworld : helloworld.o
          
    cc -o helloworld helloworld .o
          
helloworld.o : helloworld.c 
          
    cc -c main.c
          
clean:
          
    rm helloworld helloworl.o
                 
之后我们执行make就可以编译helloworld.c了,执行make clean就可以清除编译结果了(其实就是删除helloworld helloworl.o)。
  
可能有人问为什么执行make就会生成helloworld呢?这得从make的默认处理说起:make将makefile的第一个target作为作为最终的
  
target,凡是这个规则依赖的规则都将被执行,否则就不会执行。所以在执行make的时候,clean这个规则就没有被执行。
  

面的是最简单的makefile,复杂点makefile就开始使用高级点的技巧了,例如使用变量,使用隐式规则,执行负责点shell命令(常见的是字
符串处理和文件处理等),这里不打算介绍这些规则,后面在分析android的makefile时会结合具体代码进行具体分析,大家可以先看看陈皓的《跟
我一起写makefile》来了解了解。
  
makefile的大体的结构是程序树形的,如下:
  
                                                     


  
这样写起makefile也简单,我们将要达到的目标作为第一个规则,然后将目标分解成子目标,然后一个个写规则,依次类推,直到最下面的规则很容易实现为止。这其实和算法里面的分治法很像,将一个复杂的问题分而治之。
  

到树,我想到了编译原理里面的语法分析,语法分析里面有自顶而下的分析方法和自底而下的分析方法。当然makefile并不是要做语法分析,而是要做与语
法分析分析相反的事。(语法分析要做的是一个句子是不是根据语法可以推出来,而makefile要做的是根据规则生成一个command
执行队列。)不过makefile的规则和词法分析还是很像的。下面出一道编译原理上面的一个例子,大家可以理解一下makefile和词法分析的不同点
和相同点:
  
  ->     
      -> |||ε     
    ->      
    -> |ε     
    -> +     
    -> -     
   -> >     
    -> >=
  

  
最后,介绍一下autoconfautomake,使用这两个工具可以自动生成makefile。
  

  

上面的图可以看出,通过autoscan,我们可以根据代码生成一个叫做configure.scan的文件,然后我们编辑这个文件,参数一个
configure.in的文件。接着我们写一个makefile.am的文件,然后就可以用automake生成makefile.in,最后,根据
makefile.in和configure就可以生成makefile了。在很多开源的工程里面,我们都可以看到
makefile.am,configure.in,makefine.in,configure文件,还有可能看到一个十分复杂的makefile文
件,许多人学习makefile的时候想通过看这个文件来学习,最终却发现太复杂了。如果我们知道这个文件是自动生成的,就理解这个makefile文件
为什么这个复杂了。
  
欲更加详细的理解automake等工具,可以参考
http://www.ibm.com/developerworks/cn/linux/l-makefile/

1.2 用makefile构建交叉编译环境
  
这节的内容请参考
http://blog.csdn.net/absurd/category/228434.aspx
里面的交叉编译场景分析,我只是说一下我做的步骤:
  
1.下载交叉编译环境(
http://www.codesourcery.com/downloads/public/gnu_toolchain/arm-none-linux-gnueabi
)并安装,一般解压就可以了,然后将里面的bin目录加到环境变量的PATH里面,我的做法是在~/.bashrc最下面加一行:export PATH=$PATH:~/arm-2009q1/bin。
  
2.在用户的home目录(cd ~)建一个目录cross-compile
  
3.在cross-compile创建一个文件cross.env,内容如下:
  
export WORK_DIR=~/cross-compile   
export ROOTFS_DIR=$WORK_DIR/rootfs    
export ARCH=arm    
export PKG_CONFIG_PATH=$ROOTFS_DIR/usr/local/lib/pkgconfig:$ROOTFS_DIR/usr/lib/pkgconfig:$ROOTFS_DIR/usr/X11R6/lib/pkgconfig    
if [ ! -e "$ROOTFS_DIR/usr/local/include" ]; then mkdir -p $ROOTFS_DIR/usr/local/include;fi;    
if [ ! -e "$ROOTFS_DIR/usr/local/lib" ]; then mkdir -p $ROOTFS_DIR/usr/local/lib; fi;    
if [ ! -e "$ROOTFS_DIR/usr/local/etc" ]; then mkdir -p $ROOTFS_DIR/usr/local/etc; fi;    
if [ ! -e "$ROOTFS_DIR/usr/local/bin" ]; then mkdir -p $ROOTFS_DIR/usr/local/bin; fi;    
if [ ! -e "$ROOTFS_DIR/usr/local/share" ]; then mkdir -p $ROOTFS_DIR/usr/local/share; fi;    
if [ ! -e "$ROOTFS_DIR/usr/local/man" ]; then mkdir -p $ROOTFS_DIR/usr/local/man; fi;    
if [ ! -e "$ROOTFS_DIR/usr/include" ]; then mkdir -p $ROOTFS_DIR/usr/include; fi;    
if [ ! -e "$ROOTFS_DIR/usr/lib" ]; then mkdir -p $ROOTFS_DIR/usr/lib; fi;    
if [ ! -e "$ROOTFS_DIR/usr/etc" ]; then mkdir -p $ROOTFS_DIR/usr/etc; fi;    
if [ ! -e "$ROOTFS_DIR/usr/bin" ]; then mkdir -p $ROOTFS_DIR/usr/bin; fi;    
if [ ! -e "$ROOTFS_DIR/usr/share" ]; then mkdir -p $ROOTFS_DIR/usr/share; fi;    
if [ ! -e "$ROOTFS_DIR/usr/man" ]; then mkdir -p $ROOTFS_DIR/usr/man; fi;
  
4.开启命令行,进入cross-compile目录下,执行. cross.env
  
5.将编译linux时生产的头文件,so等拷贝到cross-compile目录下rootfs/usr对应的目录(头文件一般可以拷pc的,so一定要拷arm版的)。
  
5.下载要编译的源代码,并放在cross-compile目录下
  
6.按照
里面的方法写makefile文件,放在cross-compile目录下
作者:u011467537 发表于2016/11/21 19:56:40 原文链接
阅读:24 评论:0 查看评论

Android--自定义View控件

$
0
0

声明:本文中使用的Demo的Git地址:https://github.com/NoClay/TestView.git

1.自定义View中需要知道的几个类

1.MeasureSpec

简述:作为一个尺寸类,将测量模式和尺寸合而为一,在我们自定义的控件,MeasureSpce的四种测量模式分别对应如下:
UNSPECIFIED ---------------------------------------------------------> 系统内部使用,要多大给多大,父容器对子View没有任何影响,要多大给多大
EXACTLY ---------------------------------------------------------------->精确值,对应例如34dp,或者math_parent
AT_MOST ------------------====--------------------------------------->父容器给定子View了一个可用大小的SpecSize, View的大小相当于wrap_content
   public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
		
		//父容器不对View有任何限制,要多大给多大,一般用于系统内部
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
		
		//父容器已经检测出View所需要的精确大小,这个时候view的最终大小就是指定的SpecSize的值
		//对应于使用match_parent和具体的数值如37dp这种
        public static final int EXACTLY     = 1 << MODE_SHIFT;
		
		//父容器指定了一个可用大小的SpecSize,View的大小不能大于这个值,相当于wrap_content
        public static final int AT_MOST     = 2 << MODE_SHIFT;
		
		//获取一个SpecSize的模式
        @MeasureSpecMode
        public static int getMode(int measureSpec) ;
		//获取一个SpecSize的大小
        public static int getSize(int measureSpec) ;
		//对一个SpecSize的大小进行调整
        static int adjust(int measureSpec, int delta);
    }


2.Paint

简述:Paint即画笔,在绘图过程中起到了极其重要的作用,画笔主要保存了颜色, 样式等绘制信息,指定了如何绘制文本和图形,画笔对象有很多设置方法,  大体上可以分为两类,一类与图形绘制相关Paint,一类与文本绘制相关TextPaint。相当于我们常用的ps软件中的画笔,我们可以对其设置许多东西,比如画笔颜色、抗锯齿、画笔粗细、画笔线形等

/**  

     * Paint类介绍  

     *   

     * Paint即画笔,在绘图过程中起到了极其重要的作用,画笔主要保存了颜色,  

     * 样式等绘制信息,指定了如何绘制文本和图形,画笔对象有很多设置方法,  

     * 大体上可以分为两类,一类与图形绘制相关,一类与文本绘制相关。         

     *   

     * 1.图形绘制  

     * setARGB(int a,int r,int g,int b);  

     * 设置绘制的颜色,a代表透明度,r,g,b代表颜色值。  

     *   

     * setAlpha(int a);  

     * 设置绘制图形的透明度。  

     *   

     * setColor(int color);  

     * 设置绘制的颜色,使用颜色值来表示,该颜色值包括透明度和RGB颜色。  

     *   

    * setAntiAlias(boolean aa);  

     * 设置是否使用抗锯齿功能,会消耗较大资源,绘制图形速度会变慢。  

     *   

     * setDither(boolean dither);  

     * 设定是否使用图像抖动处理,会使绘制出来的图片颜色更加平滑和饱满,图像更加清晰  

     *   

     * setFilterBitmap(boolean filter);  

     * 如果该项设置为true,则图像在动画进行中会滤掉对Bitmap图像的优化操作,加快显示  

     * 速度,本设置项依赖于dither和xfermode的设置  

     *   

     * setMaskFilter(MaskFilter maskfilter);  

     * 设置MaskFilter,可以用不同的MaskFilter实现滤镜的效果,如滤化,立体等       *   

     * setColorFilter(ColorFilter colorfilter);  

     * 设置颜色过滤器,可以在绘制颜色时实现不用颜色的变换效果  

     *   

     * setPathEffect(PathEffect effect);  

     * 设置绘制路径的效果,如点画线等  

     *   

     * setShader(Shader shader);  

     * 设置图像效果,使用Shader可以绘制出各种渐变效果  

     *  

     * setShadowLayer(float radius ,float dx,float dy,int color);  

     * 在图形下面设置阴影层,产生阴影效果,radius为阴影的角度,dx和dy为阴影在x轴和y轴上的距离,color为阴影的颜色  

     *   

     * setStyle(Paint.Style style);  

     * 设置画笔的样式,为FILL,FILL_OR_STROKE,或STROKE  

     *   

     * setStrokeCap(Paint.Cap cap);  

     * 当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式,如圆形样式  

     * Cap.ROUND,或方形样式Cap.SQUARE  

     *   

     * setSrokeJoin(Paint.Join join);  

     * 设置绘制时各图形的结合方式,如平滑效果等  

     *   

     * setStrokeWidth(float width);  

     * 当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的粗细度  

     *   

     * setXfermode(Xfermode xfermode);  

     * 设置图形重叠时的处理方式,如合并,取交集或并集,经常用来制作橡皮的擦除效果  

     *   

     * 2.文本绘制  

     * setFakeBoldText(boolean fakeBoldText);  

     * 模拟实现粗体文字,设置在小字体上效果会非常差  

     *   

     * setSubpixelText(boolean subpixelText);  

     * 设置该项为true,将有助于文本在LCD屏幕上的显示效果  

     *   

     * setTextAlign(Paint.Align align);  

     * 设置绘制文字的对齐方向  

     *   

   * setTextScaleX(float scaleX);  

    * 设置绘制文字x轴的缩放比例,可以实现文字的拉伸的效果  

     *   

     * setTextSize(float textSize);  

     * 设置绘制文字的字号大小  

     *   

     * setTextSkewX(float skewX);  

     * 设置斜体文字,skewX为倾斜弧度  

     *   

     * setTypeface(Typeface typeface);  

     * 设置Typeface对象,即字体风格,包括粗体,斜体以及衬线体,非衬线体等  

     *   

     * setUnderlineText(boolean underlineText);  

     * 设置带有下划线的文字效果  

     *   

     * setStrikeThruText(boolean strikeThruText);  

     * 设置带有删除线的效果  

     *   

     */  

3.Canvas

简述:相当于我们的ps软件,设置好画笔,即可通过代码在画布上画圆等
public class Canvas{
		弧线(arcs)、填充颜色(argb和color)、
		Bitmap、圆(circle和oval)、点(point)、
		线(line)、矩形(Rect)、图片(Picture)、
		圆角矩形 (RoundRect)、文本(text)、
		顶点(Vertices)、路径(path)。
		Canvas位置转换的方法:
		rorate(旋转)、
		scale(缩放)、
		translate(变换)、
		skew(扭曲)等
	} 

2.View绘制的三大流程

首先先看各种View的继承关系。


在一个Activity手机屏幕的View树,顶层View即我们看到的View。





1.View绘制的三大流程--measure


主要作用:为整个View树计算实际的大小,即设置实际的高(对应属性:mMeasuredHeight)和宽(对应属性:

  mMeasureWidth),每个View的控件的实际宽高都是由父视图和本身视图决定的。

 

     具体的调用链如下:

         ViewRoot根对象地属性mView(其类型一般为ViewGroup类型)调用measure()方法去计算View树的大小,回调View/ViewGroup对象的onMeasure()方法,该方法实现的功能如下:

   

         1、设置本View视图的最终大小,该功能的实现通过调用setMeasuredDimension()方法去设置实际的高(对应属性:mMeasuredHeight)和宽(对应属性:mMeasureWidth)  ;

         2 、如果该View对象是个ViewGroup类型,需要重写该onMeasure()方法,对其子视图进行遍历的measure()过程。对每个子视图的measure()过程,是通过调用父类ViewGroup.java类里的measureChildWithMargins()方法去实现,该方法内部只是简单地调用了View对象的measure()方法。(由于measureChildWithMargins()方法只是一个过渡层更简单的做法是直接调用View对象的measure()方法)。

              

     整个measure调用流程就是个树形的递归过程

看流程图如下,如果View树申请重新测量,则进行重新测量,从顶层View依次向下递归测量整个View树





伪代码如下:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {  
    //....  
  
    //回调onMeasure()方法    
    onMeasure(widthMeasureSpec, heightMeasureSpec);  
     
    //more  
}  

//回调View视图里的onMeasure过程  
private void onMeasure(int height , int width){  
 //设置该view的实际宽(mMeasuredWidth)高(mMeasuredHeight)  
 //1、该方法必须在onMeasure调用,否者报异常。  
 setMeasuredDimension(h , l) ;  
   
 //2、如果该View是ViewGroup类型,则对它的每个子View进行measure()过程  
 int childCount = getChildCount() ;  
   
 for(int i=0 ;i<childCount ;i++){  
  //2.1、获得每个子View对象引用  
  View child = getChildAt(i) ;  
    
  //整个measure()过程就是个递归过程  
  //该方法只是一个过滤器,最后会调用measure()过程 ;或者 measureChild(child , h, i)方法都  
  measureChildWithMargins(child , h, i) ;   
    
  //其实,对于我们自己写的应用来说,最好的办法是去掉框架里的该方法,直接调用view.measure(),如下:  
  //child.measure(h, l)  
 }  
}  
  
//该方法具体实现在ViewGroup.java里 。  
protected  void measureChildWithMargins(View v, int height , int width){  
 v.measure(h,l)     
} 

2.View绘制的三大流程--layout

主要作用 :为将整个根据子视图的大小以及布局参数将View树放到合适的位置上。

 

     具体的调用链如下:

       host.layout()开始View树的布局,继而回调给View/ViewGroup类中的layout()方法。具体流程如下

  

        1 、layout方法会设置该View视图位于父视图的坐标轴,即mLeft,mTop,mLeft,mBottom(调用setFrame()函数去实现)接下来回调onLayout()方法(如果该View是ViewGroup对象,需要实现该方法,对每个子视图进行布局) ;

       

       2、如果该View是个ViewGroup类型,需要遍历每个子视图chiildView,调用该子视图的layout()方法去设置它的坐标值。


流程图如下:如果请求View树重新布局,则进行重新布局,与测量流程类似,依次自顶层View递归向下测量



伪代码:

/* final 标识符 , 不能被重载 , 参数为每个视图位于父视图的坐标轴 
 * @param l Left position, relative to parent 
 * @param t Top position, relative to parent 
 * @param r Right position, relative to parent 
 * @param b Bottom position, relative to parent 
 */  
public final void layout(int l, int t, int r, int b) {  
    boolean changed = setFrame(l, t, r, b); //设置每个视图位于父视图的坐标轴  
    if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {  
        if (ViewDebug.TRACE_HIERARCHY) {  
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);  
        }  
  
        onLayout(changed, l, t, r, b);//回调onLayout函数 ,设置每个子视图的布局  
        mPrivateFlags &= ~LAYOUT_REQUIRED;  
    }  
    mPrivateFlags &= ~FORCE_LAYOUT;  
}  



// layout()过程  ViewRoot.java  
// 发起layout()的"发号者"在ViewRoot.java里的performTraversals()方法, mView.layout()  
  
private void  performTraversals(){  
   
    //...  
      
    View mView  ;  
       mView.layout(left,top,right,bottom) ;  
      
    //....  
}  
  
//回调View视图里的onLayout过程 ,该方法只由ViewGroup类型实现  
private void onLayout(int left , int top , right , bottom){  
  
 //如果该View不是ViewGroup类型  
 //调用setFrame()方法设置该控件的在父视图上的坐标轴  
   
 setFrame(l ,t , r ,b) ;  
   
 //--------------------------  
   
 //如果该View是ViewGroup类型,则对它的每个子View进行layout()过程  
 int childCount = getChildCount() ;  
   
 for(int i=0 ;i<childCount ;i++){  
  //2.1、获得每个子View对象引用  
  View child = getChildAt(i) ;  
  //整个layout()过程就是个递归过程  
  child.layout(l, t, r, b) ;  
 }  
}  


3.View绘制的三大流程--draw

由ViewRoot对象的performTraversals()方法调用draw()方法发起绘制该View树,值得注意的是每次发起绘图时,并不会重新绘制每个View树的视图,而只会重新绘制那些“需要重绘”的视图,View类内部变量包含了一个标志位DRAWN,当该视图需要重绘时,就会为该View添加该标志位。

 

   调用流程 :

     mView.draw()开始绘制,draw()方法实现的功能如下:

          1 、绘制该View的背景

          2 、为显示渐变框做一些准备操作(见5,大多数情况下,不需要改渐变框)          

          3、调用onDraw()方法绘制视图本身  (每个View都需要重载该方法,ViewGroup不需要实现该方法)

          4、调用dispatchDraw ()方法绘制子视图(如果该View类型不为ViewGroup,即不包含子视图,不需要重载该方法)值得说明的是,ViewGroup类已经为我们重写了dispatchDraw ()的功能实现,应用程序一般不需要重写该方法,但可以重载父类函数实现具体的功能。

  dispatchDraw()方法内部会遍历每个子视图,调用drawChild()去重新回调每个子视图的draw()方法(注意,这个 地方“需要重绘”的视图才会调用draw()方法)。值得说明的是,ViewGroup类已经为我们重写了dispatchDraw()的功能实现,应用程序一般不需要重写该方法,但可以重载父类函数实现具体的功能。


伪代码:
// draw()过程     ViewRoot.java  
// 发起draw()的"发号者"在ViewRoot.java里的performTraversals()方法, 该方法会继续调用draw()方法开始绘图  
private void  draw(){  
   
    //...  
 View mView  ;  
    mView.draw(canvas) ;    
      
    //....  
}  
  
//回调View视图里的onLayout过程 ,该方法只由ViewGroup类型实现  
private void draw(Canvas canvas){  
 //该方法会做如下事情  
 //1 、绘制该View的背景  
 //2、为绘制渐变框做一些准备操作  
 //3、调用onDraw()方法绘制视图本身  
 //4、调用dispatchDraw()方法绘制每个子视图,dispatchDraw()已经在Android框架中实现了,在ViewGroup方法中。  
      // 应用程序程序一般不需要重写该方法,但可以捕获该方法的发生,做一些特别的事情。  
 //5、绘制渐变框    
}  
  
//ViewGroup.java中的dispatchDraw()方法,应用程序一般不需要重写该方法  
@Override  
protected void dispatchDraw(Canvas canvas) {  
 //   
 //其实现方法类似如下:  
 int childCount = getChildCount() ;  
   
 for(int i=0 ;i<childCount ;i++){  
  View child = getChildAt(i) ;  
  //调用drawChild完成  
  drawChild(child,canvas) ;  
 }       
}  
//ViewGroup.java中的dispatchDraw()方法,应用程序一般不需要重写该方法  
protected void drawChild(View child,Canvas canvas) {  
 // ....  
 //简单的回调View对象的draw()方法,递归就这么产生了。  
 child.draw(canvas) ;  
   
 //.........  
}  


3.自定义View的一些重要方法

 1.invalidate()方法 :


   说明:请求重绘View树,即draw()过程,假如视图发生大小没有变化就不会调用layout()过程,并且只绘制那些“需要重绘的”视图,即谁(View的话,只绘制该View ;ViewGroup,则绘制整个ViewGroup)请求invalidate()方法,就绘制该视图。

 

     一般引起invalidate()操作的函数如下:

           1、直接调用invalidate()方法,请求重新draw(),但只会绘制调用者本身。

           2、setSelection()方法:请求重新draw(),但只会绘制调用者本身。

           3、setVisibility()方法 :当View可视状态在INVISIBLE转换VISIBLE时,会间接调用invalidate()方法,

                    继而绘制该View。

           4 、setEnabled()方法 :请求重新draw(),但不会重新绘制任何视图包括该调用者本身。



2.requestLayout()方法 :会导致调用measure()过程 和 layout()过程 。

 

           说明:只是对View树重新布局layout过程包括measure()和layout()过程,不会调用draw()过程,但不会重新绘制任何视图包括该调用者本身。

 

    一般引起操作的方法如下:

         setVisibility()方法:

            当View的可视状态在INVISIBLE/VISIBLE 转换为GONE状态时,会间接调用requestLayout() 和invalidate方法。 同时,由于整个个View树大小发生了变化,会请求measure()过程以及draw()过程,同样地,只绘制需要“重新绘制”的视图。


3.requestFocus()函数说明:

 

          说明:请求View树的draw()过程,但只绘制“需要重绘”的视图。



4.实例

1.继承自View重写onDraw

继承自View制作一个心电图的自定义View
public class HeartWavesView extends View {

    private int mTableLineColor = Color.RED;
    private int mWavesLineColor = Color.BLACK;
    private int mTitleColor = Color.BLACK;
    private int mTitleSize = 30;
    private int mXYTextSize = 20;

    private Context context;
    private static final String TAG = "HeartWavesView";

    private Paint paintWavesLine;
    private Paint paintTableLine;
    private TextPaint paintTitle;
    private TextPaint paintXYText;

    private boolean isFirstDrawPoint = true;
    private boolean isFirstDrawBackground = true;
    private int height;
    private int width;
    private int leftPadding;
    private int rightPadding;
    private int topPadding;
    private int bottomPadding;

    private int maxY = 2100;
    private int minY = -2100;
    private int maxX = 120;
    private int x_num = 25;
    private int y_num;
    private int grid_width;

    //x轴每个小格子对应的秒
    //y轴每个小格子对应的指数
    private int grid_second = 5;
    private float grid_num;
    private int zeroCurY;
    private int yStartNum;


    private int workWidth;
    private int workHeight;

    //几秒钟一次数据,默认为1秒1次
    private float dataHz = 1;

    private List<PointXY> pointList;

    private String title = "心电图";


    public HeartWavesView(Context context) {
        super(context);
        this.context = context;
        initView();
    }

    public HeartWavesView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        resolveAttrs(attrs);
        initView();
    }

    private void resolveAttrs(AttributeSet attrs) {
        TypedArray typeArray = context.obtainStyledAttributes(attrs, R.styleable.HeartWavesView);
        mTableLineColor = typeArray.getColor(R.styleable.HeartWavesView_tableLineColor, Color.RED);
        mTitleColor = typeArray.getColor(R.styleable.HeartWavesView_titleColor, Color.BLACK);
        mWavesLineColor = typeArray.getColor(R.styleable.HeartWavesView_wavesLineColor, Color.BLACK);
        mTitleSize = typeArray.getDimensionPixelSize(R.styleable.HeartWavesView_titleSize, 30);
        mXYTextSize = typeArray.getDimensionPixelSize(R.styleable.HeartWavesView_xyTextSize, 20);
        typeArray.recycle();
    }

    public HeartWavesView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        resolveAttrs(attrs);
        initView();
    }

    private void initView() {
        //生成抗锯齿的画笔
        pointList = new ArrayList<>();
        paintWavesLine = new Paint(Paint.ANTI_ALIAS_FLAG);
        //设置画笔粗细
        paintWavesLine.setStrokeWidth(2.5f);
        //设置画笔颜色
        paintWavesLine.setColor(mWavesLineColor);

        paintTableLine = new Paint();
        paintTableLine.setColor(mTableLineColor);
        paintTableLine.setAntiAlias(true);
        paintWavesLine.setStrokeWidth(4);

        paintTitle = new TextPaint();
        paintTitle.setTextSize(mTitleSize);
        paintTitle.setColor(mTitleColor);

        paintXYText = new TextPaint();
        paintXYText.setColor(mTitleColor);
        paintXYText.setTextSize(mXYTextSize);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (isFirstDrawBackground) {
            height = getHeight();
            width = getWidth();
            leftPadding = rightPadding = bottomPadding = topPadding = 100;
        }
        drawBackground(canvas);
        drawWaves(canvas);
    }

    private void drawWaves(Canvas canvas) {
        PointXY start = new PointXY();
        PointXY end = new PointXY();
        for (int i = 0; i < pointList.size() - 1; i++) {
            start = pointList.get(i);
            end = pointList.get(i + 1);
            canvas.drawLine(start.getX(), start.getY(), end.getX(), end.getY(), paintWavesLine);
        }
    }

    private void drawBackground(Canvas canvas) {
        if (isFirstDrawBackground) {
            isFirstDrawBackground = false;
            x_num = maxX / grid_second;
            x_num = x_num % 5 == 0 ? x_num : (x_num % 5 > 3 ? (x_num / 5 + 1) * 5 : x_num / 5 * 5);
            grid_width = (width - leftPadding - rightPadding) / x_num;
            y_num = (height - topPadding - rightPadding) / grid_width;
            y_num = y_num % 5 == 0 ? y_num : (y_num % 5 > 3 ? (y_num / 5 + 1) * 5 : y_num / 5 * 5);
            //获取工作区的宽和高
            workWidth = grid_width * x_num;
            workHeight = grid_width * y_num;
            //获取xy轴比例尺
            //获得y轴0标识位的位置
            if (maxY > 0 && minY >= 0) {
                yStartNum = maxY;
                grid_num = maxY / y_num;
                zeroCurY = y_num;
            } else if (maxY <= 0 && minY < 0) {
                yStartNum = 0;
                grid_num = -minY / y_num;
                zeroCurY = 0;
            } else {
                zeroCurY = y_num / 2;
                zeroCurY = zeroCurY % 5 == 0 ? zeroCurY :
                        (zeroCurY % 5 > 3) ? (zeroCurY / 5 + 1) * 5 : (zeroCurY / 5 * 5);
                grid_num = Math.max(maxY, minY) / Math.min(y_num - zeroCurY, zeroCurY);
                yStartNum = (int) (zeroCurY * grid_num);
            }
        }
        for (int i = 0; i <= x_num; i++) {
            paintTableLine.setStrokeWidth(1f);
            if (i % 5 == 0) {
                paintTableLine.setStrokeWidth(3f);
                String label = grid_second * i + "";
                canvas.drawText(label,
                        leftPadding + i * grid_width - mXYTextSize / 2,
                        workHeight + bottomPadding / 2 + topPadding,
                        paintXYText);
            }
            canvas.drawLine(leftPadding + i * grid_width, topPadding,
                    leftPadding + i * grid_width, topPadding + workHeight, paintTableLine);
        }
        for (int i = 0; i <= y_num; i++) {
            paintTableLine.setStrokeWidth(1f);
            if (i % 5 == 0) {
                paintTableLine.setStrokeWidth(3f);
                String label = yStartNum - i * grid_num + "";
                canvas.drawText(label, leftPadding / 5, topPadding + i * grid_width, paintXYText);
            }
            canvas.drawLine(leftPadding, topPadding + i * grid_width,
                    leftPadding + workWidth, topPadding + i * grid_width, paintTableLine);
        }
        canvas.drawText(title, width / 2 - mTitleSize * title.length() / 2,
                topPadding / 2, paintTitle);
    }

    public void drawNextPoint(float y) {
        if (!isFirstDrawBackground) {
            if (isFirstDrawPoint) {
                isFirstDrawPoint = false;
                PointXY point = new PointXY();
                point.setX(leftPadding);
                point.setY(zeroCurY * grid_width + topPadding);
                pointList.add(point);
            }
            PointXY lastPoint = pointList.get(pointList.size() - 1);
            Log.d(TAG, "drawNextPoint: size" + pointList.size());
            if (pointList.size() == maxX- 1) {
                pointList.clear();
                lastPoint.setX(leftPadding);
            }
            PointXY nowPoint = new PointXY();
            nowPoint.setX(dataHz / grid_second * grid_width + lastPoint.getX());
            nowPoint.setY((yStartNum - y) / grid_num * grid_width + topPadding);
            pointList.add(nowPoint);
            invalidate();

        }

    }

    public Paint getPaintWavesLine() {
        return paintWavesLine;
    }

    public void setPaintWavesLine(Paint paintWavesLine) {
        this.paintWavesLine = paintWavesLine;
    }

    public Paint getPaintTableLine() {
        return paintTableLine;
    }

    public void setPaintTableLine(Paint paintTableLine) {
        this.paintTableLine = paintTableLine;
    }

    public TextPaint getPaintTitle() {
        return paintTitle;
    }

    public void setPaintTitle(TextPaint paintTitle) {
        this.paintTitle = paintTitle;
    }

    public TextPaint getPaintXYText() {
        return paintXYText;
    }

    public void setPaintXYText(TextPaint paintXYText) {
        this.paintXYText = paintXYText;
    }

    public int getLeftPadding() {
        return leftPadding;
    }

    public void setLeftPadding(int leftPadding) {
        this.leftPadding = leftPadding;
    }

    public int getRightPadding() {
        return rightPadding;
    }

    public void setRightPadding(int rightPadding) {
        this.rightPadding = rightPadding;
    }

    public int getTopPadding() {
        return topPadding;
    }

    public void setTopPadding(int topPadding) {
        this.topPadding = topPadding;
    }

    public int getBottomPadding() {
        return bottomPadding;
    }

    public void setBottomPadding(int bottomPadding) {
        this.bottomPadding = bottomPadding;
    }

    public int getMaxY() {
        return maxY;
    }

    public void setMaxY(int maxY) {
        this.maxY = maxY;
    }

    public int getMinY() {
        return minY;
    }

    public void setMinY(int minY) {
        this.minY = minY;
    }

    public int getMaxX() {
        return maxX;
    }

    public void setMaxX(int maxX) {
        this.maxX = maxX;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public class PointXY {
        private float x;
        private float y;

        public float getX() {
            return x;
        }

        public void setX(float x) {
            this.x = x;
        }

        public float getY() {
            return y;
        }

        public void setY(float y) {
            this.y = y;
        }
    }
}


2.继承自ViewGroup实现特定的layout


实现一个水平的滑动栏容器
public class HorizontalScrollViewEx extends ViewGroup {
    private static final String TAG = "HorizontalScrollViewEx";
    private int mChildrenSize;
    private int mChildWidth;
    private int mChildIndex;
    //记录上次滑动的坐标
    private int mLastX = 0;
    private int mLastY = 0;
    //记录上次滑动的坐标(onInterceptTouchEvent)
    private int mLastXIntercept = 0;
    private int mLastYIntercept = 0;

    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;


    public HorizontalScrollViewEx(Context context) {
        super(context);
        init();
    }

    public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        if (mScroller == null) {
            mScroller = new Scroller(getContext());
            mVelocityTracker = VelocityTracker.obtain();
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                intercepted = false;
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                    //动画滚动到最终位置结束动画,这里代表本身HorizontalScrollViewEx处理触摸事件
                    intercepted = true;
                }
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastXIntercept;
                int deltaY = y - mLastYIntercept;
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    //水平滑动则处理此事件
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                intercepted = false;
                break;
            }
            default:
                break;
        }
        Log.d(TAG, "onInterceptTouchEvent: intercepted = " + intercepted);
        mLastX = x;
        mLastY = y;
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercepted;
    }


    /**
     * 本身在处理触摸事件
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mVelocityTracker.addMovement(event);
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch(event.getAction()){
            case MotionEvent.ACTION_DOWN:{
                if(!mScroller.isFinished()){
                    mScroller.abortAnimation();
                }
                break;
            }
            case MotionEvent.ACTION_MOVE:{
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                scrollBy(-deltaX, 0);
                break;
            }
            case MotionEvent.ACTION_UP:{
                int scrollX = getScrollX();
                mVelocityTracker.computeCurrentVelocity(1000);
                float xVelocity = mVelocityTracker.getXVelocity();
                if(Math.abs(xVelocity) >= 50){
                    mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
                }else{
                    mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
                }
                mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
                int dx = mChildIndex * mChildWidth - scrollX;
                smoothScrollBy(dx, 0);
                mVelocityTracker.clear();
                break;
            }
            default:break;
        }
        mLastX = x;
        mLastY = y;
        return true;
    }


    /**
     * 重写onMeasure方法,对容器内的测量进行修正
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        /**
         * super.onMeasure方法内部调用了setMeasuredDimension(getDefaultSize(
         * getSuggestedMinimumWidth(), widthMeasureSpec),
         * getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
         */
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measuredWidth = 0;
        int measuredHeight = 0;
        final int childCount = getChildCount();
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpaceMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpaceMode = MeasureSpec.getMode(heightMeasureSpec);
        if(childCount == 0){
            //no child -- no width, no height
            setMeasuredDimension(0, 0);
        }else if(widthSpaceMode == MeasureSpec.AT_MOST &&
                heightSpaceMode == MeasureSpec.AT_MOST){
            //width is wrap and height is wrap
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(measuredWidth, measuredHeight);
        }else if(heightSpaceMode == MeasureSpec.AT_MOST){
            //height is wrap but width not
            final View childView = getChildAt(0);
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(widthSpaceSize, measuredHeight);
        }else if(widthMeasureSpec == MeasureSpec.AT_MOST){
            //width is wrap but height not
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth();
            setMeasuredDimension(measuredWidth, heightSpaceSize);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childLeft = 0;
        final int childCount = getChildCount();
        mChildrenSize = childCount;
        for(int i = 0; i < childCount; i ++){
            final View childView = getChildAt(i);
            if(childView.getVisibility() != View.GONE){
                final int childWidth = childView.getMeasuredWidth();
                mChildWidth = childWidth;
                childView.layout(childLeft, 0, childLeft + childWidth,
                        childView.getMeasuredHeight());
                childLeft += childWidth;
            }
        }
    }

    /**
     * 两个startScrollBy方法,一个加上了时间间隔,一个并没有
     * 之前整错了,用错了方法
     * @param dx
     * @param dy
     */
    private void smoothScrollBy(int dx, int dy) {
        mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        mVelocityTracker.recycle();
        super.onDetachedFromWindow();
    }


}


3.继承自特定的ViewGroup


布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_gravity="center"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="3dp">

    <ImageView
        android:id="@+id/image_view_image"
        android:layout_gravity="center_horizontal"
        android:src="@drawable/image"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:padding="5dp"/>

    <TextView
        android:textSize="10sp"
        android:text="测试"
        android:id="@+id/image_view_title"
        android:layout_width="match_parent"
        android:layout_height="10dp"
        android:gravity="center_horizontal" />

</LinearLayout>

实现代码:

package noclay.testview;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

/**
 * Created by 82661 on 2016/10/18.
 */

public class MyImageView extends LinearLayout {
    private static final String TAG = "MyImageView";
    private TextView textView;
    private ImageView imageView;
    private LinearLayout linearLayout;
    private static Context context;

    public MyImageView(Context context) {
        super(context);
        this.context = context;
        init();
    }

    public MyImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        init();
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MyImageView);
        textView.setText(array.getText(R.styleable.MyImageView_title));
        imageView.setImageDrawable(getResources().
                getDrawable(array.getResourceId(R.styleable.MyImageView_image, R.drawable.image)));
        array.recycle();
    }

    public MyImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        init();
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MyImageView);
        textView.setText(array.getText(R.styleable.MyImageView_title));
        imageView.setImageDrawable(getResources().
                getDrawable(array.getResourceId(R.styleable.MyImageView_image, R.drawable.image)));
        array.recycle();
    }

    public void init(){
        LayoutInflater.from(getContext()).inflate(R.layout.my_iamge_view, this, true);
        textView = (TextView) findViewById(R.id.image_view_title);
        imageView = (ImageView) findViewById(R.id.image_view_image);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measuredWidth = 0;
        int measuredHeight = 0;
        final int childCount = getChildCount();
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpaceMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpaceMode = MeasureSpec.getMode(heightMeasureSpec);
        Log.d(TAG, "onMeasure: width = " + widthSpaceSize);
        Log.d(TAG, "onMeasure: height = " + heightSpaceSize);
        if(widthSpaceMode == MeasureSpec.EXACTLY && heightSpaceMode == MeasureSpec.EXACTLY){
            //在有具体数值的时候调整子项的大小
            if(7 * widthSpaceSize <= 5 * heightSpaceSize){//规定长宽比例为5:7
                setMeasure(widthSpaceSize);
            }else{
                setMeasure(heightSpaceSize);
            }
        }
    }

    private void setMeasure(int value){
        Log.d(TAG, "onMeasure: true");
        LayoutParams lp = (LayoutParams) imageView.getLayoutParams();
        lp.width = value * 4 / 5 ;
        lp.height = value * 4 / 5;
        lp.bottomMargin = lp.topMargin = value * 1 / 10;
        imageView.setLayoutParams(lp);

        lp = (LayoutParams) textView.getLayoutParams();
        lp.width = value;
        lp.height = value * 2 / 5;
        textView.setLayoutParams(lp);
        textView.setTextSize(px2sp(lp.height / 2));
    }

    public static int px2sp(float value) {
        final float scale =context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (value / scale + 0.5f);
    }
}


4.继承自特定的View

一个圆形图片的View
package noclay.testview;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.ImageView;

/**
 * Created by 82661 on 2016/10/25.
 */

public class MyCircleImageView extends ImageView {
    private Paint paint;

    public MyCircleImageView(Context context) {
        this(context, null);
    }

    public MyCircleImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyCircleImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        paint = new Paint();

    }

    /**
     * 绘制圆形图片
     *
     * @author caizhiming
     */
    @Override
    protected void onDraw(Canvas canvas) {

        Drawable drawable = getDrawable();
        if (drawable != null) {
            Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
            Bitmap b = getCircleBitmap(bitmap);
            final Rect rectSrc = new Rect(0, 0, b.getWidth(), b.getHeight());
            final Rect rectDest = new Rect(0, 0, getWidth(), getHeight());
            paint.reset();
            canvas.drawBitmap(b, rectSrc, rectDest, paint);

        } else {
            super.onDraw(canvas);
        }
    }

    /**
     * 获取圆形图片方法
     *
     * @param bitmap
     * @return Bitmap
     * @author caizhiming
     */
    private Bitmap getCircleBitmap(Bitmap bitmap) {
        Bitmap output = Bitmap.createBitmap(bitmap.getWidth(),
                bitmap.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(output);
        final int color = 0xff424242;
        final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
        paint.setAntiAlias(true);
        //设置背景色
        canvas.drawARGB(0, 0, 0, 0);
        paint.setColor(color);
        int x = bitmap.getWidth();
		
        canvas.drawCircle(x / 2, x / 2, x / 2, paint);
		//设置遮罩
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        canvas.drawBitmap(bitmap, rect, rect, paint);
        return output;
    }
}



作者:qq_27035123 发表于2016/11/21 20:30:35 原文链接
阅读:49 评论:0 查看评论

java8新特性(拉姆达表达式lambda)

$
0
0

一、函数式接口

函数式接口(functional interface 也叫功能性接口,其实是同一个东西)。简单来说,函数式接口是只包含一个方法的接口。比如Java标准库中的java.lang.Runnable和 java.util.Comparator都是典型的函数式接口。
java 8提供 @FunctionalInterface作为注解,这个注解是非必须的,只要接口符合函数式接口的标准(即只包含一个方法的接口),虚拟机会自动判断, 但 最好在接口上使用注解@FunctionalInterface进行声明,以免团队的其他人员错误地往接口中添加新的方法。

Java中的lambda无法单独出现,它需要一个函数式接口来盛放,lambda表达式方法体其实就是函数接口的实现.

下面的接口就是一个函数式接口


@FunctionalInterface //添加此注解后,接口中只能有一个抽象方法。
public interface A {
    void call();

}

二、lambda语法

包含三部分:
1、一个括号内用逗号分隔的形式参数,参数是函数式接口里面方法的参数
2、一个箭头符号:->
3、方法体,可以是表达式和代码块。

(parameters) -> expression 或者 (parameters) -> { statements; } 

通过下面的代码可以看到lambda表达式设计的代码更==简洁==,而且可读性更好。



public class Demo1 {
    public static void main(String[] args) {
        runThreadByLambda();
        runThreadByInnerClass();
    }

    public static void runThreadByLambda() {
        /*
         Runnable就是一个函数式接口:他只有一个方法run()方法。
         1、因为run()方法没有参数,所以   ->前面的()中不需要声明形参
         2、run返回的是void,所以不需要return。
         3、->后面写的代码其实就是定义在run方法内的代码。因为此处代码只有一行,所以{}也可以省略。如果此处多与一行,则无法省略。
         */
        Runnable runnable = () -> System.out.println("这个是用拉姆达实现的线程");
        new Thread(runnable).start();
    }

    public static void runThreadByInnerClass() {
        Runnable runnable = new Runnable() {

            @Override
            public void run() {
                System.out.println("这个是用内部类实现的线程");

            }
        };
        new Thread(runnable).start();
    }
}

三、方法引用

其实是lambda表达式的一种简化写法。所引用的方法其实是lambda表达式的方法体实现,语法也很简单,左边是容器(==可以是类名,实例名==),中间是”::”,右边是相应的方法名。如下所示:

ObjectReference::methodName

一般方法的引用格式:

  1. 如果是静态方法,则是ClassName::methodName。如 Object ::equals
  2. 如果是实例方法,则是Instance::methodName。如Object obj=new Object();obj::equals;
  3. 构造函数.则是ClassName::new

“`

public class Demo2 {
public static void main(String[] args) {
/*
* 方法引用
*/
Runnable runnable = Demo2::run;
new Thread(runnable).start();
}

public static void run(){
    System.out.println("方法引用的代码...");
}

}

““

可以看出,doSomething方法就是lambda表达式的实现,这样的好处就是,如果你觉得lambda的方法体会很长,影响代码可读性,方法引用就是个解决办法

五、使用lambda改进的集合框架



@FunctionalInterface
public interface A {
    void call();

    default void fun() {
        System.out.println("我是接口的默认方法1中的代码");
    }

    default void fun2() {
        System.out.println("我是接口的默认方法2中的代码");
    }

}

为什么要有这个特性?首先,之前的接口是个双刃剑,好处是面向抽象而不是面向具体编程,缺陷是,当需要修改接口时候,需要修改全部实现该接口的类,目前的 java 8之前的集合框架没有foreach方法,通常能想到的解决办法是在JDK里给相关的接口添加新的方法及实现。然而,对于已经发布的版本,是没法在给接口 添加新方法的同时不影响已有的实现。所以引进的默认方法。他们的目的是为了使接口没有引入与现有的实现不兼容发展。

java8中接口和抽象类的区别

形同点:
++1.都是抽象类型;
2.都可以有实现方法(以前接口不行);
3.都可以不需要实现类或者继承者去实现所有方法,(以前不行,现在接口中默认方法不需要实现者实现)++
不同点
++1.抽象类不可以多重继承,接口可以(无论是多重类型继承还是多重行为继承);
2.抽象类和接口所反映出的设计理念不同。其实抽象类表示的是”is-a”关系,接口表示的是”like-a”关系;
3.接口中定义的变量默认是public static final 型,且必须给其初值,所以实现类中不能重新定义,也不能改变其值;抽象类中的变量默认是 default 型,其值可以在子类中重新定义,也可以重新赋值。++

总结:默认方法给予我们修改接口而不破坏原来的实现类的结构提供了便利,目前java 8的集合框架已经大量使用了默认方法来改进了,当我们最终开始使用Java 8的lambdas表达式时,提供给我们一个平滑的过渡体验。也许将来我们会在API设计中看到更多的默认方法的应用。

五、使用lambda改进的集合框架

5.2 Stream API

流(Stream)仅仅代表着数据流,并没有数据结构,所以他遍历完一次之后便再也无法遍历(这点在编程时候需要注意,不像Collection,遍历多少次里面都还有数据),它的来源可以是Collection、array、io等等。

流作用是提供了一种操作大数据接口,让数据操作更容易和更快。它具有过滤、映射以及减少遍历数等方法,这些方法分两种:中间方法和终端方法,“流”抽象天生就该是持续的,中间方法永远返回的是Stream,因此如果我们要获取最终结果的话,必须使用终点操作才能收集流产生的最终结果。区分这两个方法是看他的返回值,如果是Stream则是中间方法,否则是终点方法。


import java.util.ArrayList;
import java.util.List;

public class Demo3 {
    public static void main(String[] args) {
        List<User> users = new ArrayList<User>();
        users.add(new User(20, "张三"));
        users.add(new User(22, "李四"));
        users.add(new User(10, "王五"));

        users.forEach((User user) -> System.out.println(user.getAge()));

    }

}

5.2 Stream API

流(Stream)仅仅代表着数据流,并没有数据结构,所以他遍历完一次之后便再也无法遍历(这点在编程时候需要注意,不像Collection,遍历多少次里面都还有数据),它的来源可以是Collection、array、io等等。

流作用是提供了一种操作大数据接口,让数据操作更容易和更快。它具有过滤、映射以及减少遍历数等方法,这些方法分两种:中间方法和终端方法,“流”抽象天生就该是持续的,中间方法永远返回的是Stream,因此如果我们要获取最终结果的话,必须使用终点操作才能收集流产生的最终结果。区分这两个方法是看他的返回值,如果是Stream则是中间方法,否则是终点方法。

filter

在数据流中实现过滤功能是首先我们可以想到的最自然的操作了。Stream接口暴露了一个filter方法,它可以接受表示操作的Predicate实现来使用定义了过滤条件的lambda表达式。


import java.util.stream.Stream;

public class StreamDemo {
    public static void main(String[] args) {
        List<User> users = new ArrayList<User>();
        users.add(new User(20, "张三"));
        users.add(new User(22, "李四"));
        users.add(new User(10, "王五"));

        Stream<User> stream = users.stream();
        stream.filter(p -> p.getAge() > 20); //过滤年龄大于20的
    }

}

map

假使我们现在过滤了一些数据,比如转换对象的时候。Map操作允许我们执行一个Function的实现(Function

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

public class StreamDemo {
    public static void main(String[] args) {
        List<User> users = new ArrayList<User>();
        users.add(new User(20, "张三"));
        users.add(new User(22, "李四"));
        users.add(new User(10, "王五"));

        Stream<User> stream = users.stream();
         //所有的年龄大于20岁的User对象,转换为字符串50对象。现在流中只有字符串对象了。
        stream.filter((User user) ->  user.getAge() > 20).map((User user) -> {return "50";});

    }

}

count

count方法是一个流的终点方法,可使流的结果最终统计,返回long


import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collector;
import java.util.stream.Stream;

public class StreamDemo {
    public static void main(String[] args) {
        List<User> users = new ArrayList<User>();
        users.add(new User(20, "张三"));
        users.add(new User(22, "李四"));
        users.add(new User(10, "王五"));


        Stream<User> stream = users.stream();
        long count = stream.filter((User user) ->  user.getAge() >= 20).map((User user) -> {return "50";})
        .count(); //返回流中元素的个数。
        System.out.println(count);



    }

}

作者:qq_35805528 发表于2016/11/21 21:00:00 原文链接
阅读:29 评论:0 查看评论

横向滑动的广告(网格控件)

$
0
0

转载请标明出处:
http://blog.csdn.net/hai_qing_xu_kong/article/details/53264494
本文出自:【顾林海的博客】

前言

很早以前写过一篇自定义广告控件的文章,这篇文章也是自定义广告控件,不同的是内部包含的是列表,具体看效果图:

这里写图片描述

这里写图片描述

这里写图片描述

使用方式

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/darker_gray">

    <glh.gvpager.view.GVPager
        android:id="@+id/gvp"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:padding="5dp"
        app:columnMargin="10dp"
        app:columnNumber="2"
        app:rowMargin="10dp"
        app:rowNumber="1" />

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/gvp"
        android:layout_centerHorizontal="true"
        android:orientation="horizontal">

        <glh.gvpager.view.IndicatorView
            android:id="@+id/indicator"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </LinearLayout>


</RelativeLayout>
package glh.gvpager;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import java.util.Random;

import glh.gvpager.view.GVPager;
import glh.gvpager.view.IndicatorView;

public class MainActivity extends AppCompatActivity {

    private IndicatorView indicator;
    private GVPager mGVPager;
    private int[] resourceId = {R.drawable.demo1, R.drawable.demo2, R.drawable.demo3, R.drawable.demo4,R.drawable.demo5, R.drawable.demo6, R.drawable.demo1, R.drawable.demo2,R.drawable.demo3, R.drawable.demo1};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        GridPagerAdapter gridPagerAdapter = new GridPagerAdapter(10);
        mGVPager = (GVPager) findViewById(R.id.gvp);
        indicator = (IndicatorView) findViewById(R.id.indicator);
        mGVPager.setIndicator(indicator);
        mGVPager.setAutoDuration(500);
        mGVPager.setPageTransformer(new CubeTransformer());
        mGVPager.setAdapter(gridPagerAdapter);
        mGVPager.play();
    }

    @Override
    protected void onDestroy() {
        mGVPager.stop();
        super.onDestroy();
    }

    public class GridPagerAdapter extends BaseAdapter {

        int mSize;

        public GridPagerAdapter(int size) {
            mSize = size;
        }

        @Override
        public int getCount() {
            return mSize;
        }

        @Override
        public Object getItem(int position) {
            return position;
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(final int position, View convertView, ViewGroup parent) {
            ViewHolder viewHolder;
            if (convertView == null) {
                viewHolder = new ViewHolder();
                convertView = getLayoutInflater().inflate(R.layout.item_gvp, null);
                viewHolder.iv_demo = (ImageView) convertView.findViewById(R.id.iv_demo);
                convertView.setTag(viewHolder);
            } else {
                viewHolder = (ViewHolder) convertView.getTag();
            }

            viewHolder.iv_demo.setImageResource(resourceId[position]);

            return convertView;
        }

    }

    static class ViewHolder {
        ImageView iv_demo;
    }
}

原理说明

左右滑动视图的最好方案非ViewPager莫属,因此创建一个继承ViewPager的类,并且定义一些属性:

gv_attrs:

<resources>

    <declare-styleable name="GridViewPager">
        <attr name="columnNumber" format="integer" />//列数
        <attr name="rowNumber" format="integer" />//行数
        <attr name="columnMargin" format="dimension" />//列间距
        <attr name="rowMargin" format="dimension" />//行间距
        <attr name="android:paddingLeft" />
        <attr name="android:paddingRight" />
        <attr name="android:padding" />
    </declare-styleable>

</resources>

在activity_main中的使用 :

<glh.gvpager.view.GVPager
    android:id="@+id/gvp"
    android:layout_width="match_parent"
    android:layout_height="300dp"
    android:padding="5dp"
    app:columnMargin="10dp"
    app:columnNumber="2"
    app:rowMargin="10dp"
    app:rowNumber="1" />

OK,目前我们是想创建一个一行两列,行间距和列间距都是10dp,四周间距是5dp的GVPager控件,创建GVPager类。

public class GVPager extends ViewPager {

    private static final int DEFAULT_COLUMN_NUMBER = 2;
    private static final int DEFAULT_ROW_NUMBER = 3;
    private int mRowNumber = DEFAULT_ROW_NUMBER;// 行
    private int mColumnNumber = DEFAULT_COLUMN_NUMBER;// 列
    private float mColumnMargin = 0;//列间距
    private float mRowMargin = 0;//行间距
    private int mPaddingLeft = 0;//左边距
    private int mPaddingRight = 0;//右边距

    public GVPager(Context context) {
        this(context, null);
    }

    public GVPager(Context context, AttributeSet attrs) {
        super(context, attrs);
        getAttributeSet(attrs);
    }

    /**
     * 获取设置的属性值
     *
     * @param _attrs AttributeSet
     */
    private void getAttributeSet(AttributeSet _attrs) {
        if (_attrs != null) {
            TypedArray typedArray = getContext().obtainStyledAttributes(_attrs, R.styleable.GridViewPager);
            int count = typedArray.getIndexCount();
            for (int i = 0; i < count; i++) {
                int attr = typedArray.getIndex(i);
                switch (attr) {
                    case R.styleable.GridViewPager_columnNumber:
                        mColumnNumber = typedArray.getInt(attr, -1);
                        break;
                    case R.styleable.GridViewPager_rowNumber:
                        mRowNumber = typedArray.getInt(attr, -1);
                        break;
                    case R.styleable.GridViewPager_columnMargin:
                        mColumnMargin = typedArray.getDimension(attr, 0);
                        break;
                    case R.styleable.GridViewPager_rowMargin:
                        mRowMargin = typedArray.getDimension(attr, 0);
                        break;
                    case R.styleable.GridViewPager_android_padding:
                        int padding = typedArray.getDimensionPixelSize(attr, 0);
                        setPadding(padding, padding, padding, padding);
                        break;
                    case R.styleable.GridViewPager_android_paddingLeft:
                        mPaddingLeft = typedArray.getDimensionPixelSize(attr, 0);
                        break;
                    case R.styleable.GridViewPager_android_paddingRight:
                        mPaddingRight = typedArray.getDimensionPixelSize(attr, 0);
                        break;
                    default:
                        break;
                }
            }
            typedArray.recycle();
        }

    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    public void setPadding(int left, int top, int right, int bottom) {
        mPaddingLeft = left;
        mPaddingRight = right;
        super.setPadding(0, top, 0, bottom);
    }


}

GVPager类继承自ViewPager,上面的代码是获取自定义的属性值,这里面重写了setPadding方法,并手动将左右边距设置0,这是为什么呢?因为我们设置的是ViewPager内部View的左右边距,而不是设置ViewPager的左右间距。

接着给ViewPager设置相应的PagerAdapter,ViewPager显示的每一个Item View都是一个HGridView,效果图可以看出ItemView实际上是类似与GridView一样,是以列表形式展示的,ViewPager的每一个View都是一个AdapterView,也就是这里的HGridView是一个继承自AdapterView的类。

GVPager适配器:

private List<HGridView> mHGridViewList = null;//内嵌的GridView

private class GridPagerAdapter extends PagerAdapter {

    @Override
    public int getCount() {
        return mHGridViewList.size();
    }

    @Override
    public boolean isViewFromObject(View arg0, Object arg1) {
        return arg0 == arg1;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        container.removeView((View) object);
    }


    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        container.addView(mHGridViewList.get(position), new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        return mHGridViewList.get(position);

    }
}

HGridView继承自AdapterView, 而AdapterView继承自ViewGroup,因此,还得实现onMeasure和onLayout方法,用于子View的测量和定位:

public class HGridView extends AdapterView<ListAdapter> {

    private ListAdapter adapter;

    public HGridView() {
        super(GVPager.this.getContext());
    }

    @Override
    public ListAdapter getAdapter() {
        return adapter;
    }

    @Override
    public void setAdapter(ListAdapter listAdapter) {
        this.adapter = listAdapter;
        int oldChildCount = getChildCount();
        int newChildCount = adapter.getCount();
        int deleteChildCount = oldChildCount - newChildCount;
        for (int i = oldChildCount; i < newChildCount; i++) {
            View child = adapter.getView(i, null, this);
            addViewInLayout(child, i, new LayoutParams(0, 0));
        }
        if (deleteChildCount > 0) {
            removeViewsInLayout(newChildCount, deleteChildCount);
        }
    }

    @Override
    public View getSelectedView() {
        if (getChildCount() > 0) {
            return getChildAt(0);
        }
        return null;
    }

    @Override
    public void setSelection(int i) {

    }

    @Override
    public int getPaddingLeft() {
        return mPaddingLeft;
    }

    @Override
    public int getPaddingRight() {
        return mPaddingRight;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        /*
        获取子View宽高
         */
        int width = (int) ((MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight() - mColumnMargin * (mColumnNumber - 1)) / mColumnNumber);
        int height = (int) ((MeasureSpec.getSize(heightMeasureSpec) - mRowMargin * (mRowNumber - 1)) / mRowNumber);
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            /*
            给子View设置宽高
             */
            LayoutParams layoutParams = child.getLayoutParams();
            layoutParams.width = width;
            layoutParams.height = height;
            child.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
        }
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        int childCount = getChildCount();
        int childLeft = 0;
        int childTop = 0;
        for (int i = 0; i < childCount && i < mRowNumber * mColumnNumber; i++) {
            View child = getChildAt(i);
            int x = i % mColumnNumber;
            if (x == 0) {
                /*
                每一行的第一个子View
                 */
                childLeft = getPaddingLeft();
            }
            LayoutParams layoutParams = child.getLayoutParams();
            child.layout(childLeft, childTop, childLeft + layoutParams.width, childTop + layoutParams.height);
            childLeft += layoutParams.width + mColumnMargin;
            if (x == mColumnNumber - 1) {
                /*
                每一行最后一个子View,要另起一行
                 */
                childTop += layoutParams.height + mRowMargin;
            }
        }
    }
}

setAdapter方法主要做了以下几件事:

  • 填充adapter中的View。
  • 如果该AdapterView之前的子View数量大于新添加的子View数,需要移除多余的View。

填充完每屏的子View后,就需要我们测量子View的大小,实现onMeasure方法,子View的宽高我们可以根据下图分析,剩下做的就是给子View定位。具体看一下代码,相信大家都能看懂。 :

这里写图片描述

由上图可以计算出子View的宽高:

宽=(AdapterView_Width-paddingLeft-paddingRight-(列Margin)*(列数-1))/列数

高=(AdapterView_Height-(行Margin)*(行数-1))/行数

既然HGridView定义完毕,接着就得给我们的AdapterView绑定适配器,适配器所要做的事情就是定义ViewPager中每一个HGridView显示多少个View以及所要显示的View:

private class GridAdapter extends BaseAdapter {

    private int page;
    private int size;
    private BaseAdapter adapter;

    public GridAdapter(int _page, int _size, BaseAdapter _adapter) {
        this.size = _size;
        this.page = _page;
        this.adapter = _adapter;
    }

    @Override
    public int getCount() {
        if (adapter.getCount() % size == 0 || page < adapter.getCount() / size) {
            return size;
        }
        return adapter.getCount() % size;
    }

    @Override
    public Object getItem(int i) {
        return adapter.getItem(page * size + i);
    }

    @Override
    public long getItemId(int i) {
        return adapter.getItemId(page * size + i);
    }

    @Override
    public View getView(int i, View view, ViewGroup viewGroup) {
        return adapter.getView(page * size + i, view, viewGroup);
    }
}



GVPager和HGridView已经定义完毕,接下来所做的事情就是给他们绑定适配器 :

private void resetAdapter() {
    // 行*列=当前屏的总个数
    int pageSize = mColumnNumber * mRowNumber;
    if (pageSize <= 0)
        return;

    if (mAdapter.getCount() == 0) {
        mHGridViewList.removeAll(mHGridViewList);
    }
    int pageCount = mAdapter.getCount() / pageSize;
    int listSize = mHGridViewList.size() - 1;
    HGridView hGridView;
    GridAdapter gridAdapter;
    for (int i = 0, page = (mAdapter.getCount() % pageSize == 0) ? pageCount-- : pageCount; i <= Math.max(listSize, page); i++) {
        if (i <= listSize && i <= page) {
            // 更新
            hGridView = mHGridViewList.get(i);
            gridAdapter = new GridAdapter(i, pageSize, mAdapter);
            hGridView.setAdapter(gridAdapter);
            mHGridViewList.set(i, hGridView);
            continue;
        }
        if (i > listSize && i <= page) {
            // 添加
            hGridView = new HGridView();
            gridAdapter = new GridAdapter(i, pageSize, mAdapter);
            hGridView.setAdapter(gridAdapter);
            mHGridViewList.add(hGridView);
            continue;
        }
        if (i > page && i <= listSize) {// 以设置的Adapter中的个数为准,超过移除View
            mHGridViewList.remove(page + 1);// 每次都移除page+1位置的GridView
            continue;
        }
    }
    super.setAdapter(new GridPagerAdapter());
    if (mSelection >= 0) {
        setSelection(mSelection);
    }
}


到这里横向滚动的网格广告已经定义完毕,剩下的自动滚动和动画就不介绍了,下面给出这个控件的一些公共方法:

设置ViewPager的Adapter
public void setAdapter(BaseAdapter _adapter)
刷新
public void notifyDataSetChanged()    
定位位置
public void setSelection(int position)
获取总页数
public int getPageCount() 
获取总个数
public int getPageSize()
设置指示器
public void setIndicator(IndicatorView _indicator) 
自动切换开启.
public void play() 
停止播放
public void stop() 
设置ViewPager的切换动画
public void setPageTransformer(PageTransformer _pageTransformer)
滑动速度
public void setAutoDuration(int _duration) 



项目下载地址


以下是完整的github项目地址,欢迎多多star和fork。
github项目源码地址:点击【项目源码】

作者:GULINHAI12 发表于2016/11/21 21:21:15 原文链接
阅读:49 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>