百度面试题 20个数组 每个500个元素 升序排列 找出前500的数

一道面试题

有20个数组,每个数组有500个元素,升序排列,现在在这20*500个数中找出排名前500的数。

转载请注明出处 http://www.alijava.com/arrays-sort/

方法思路分析

笔者想到3种方法,有更好的方法欢迎评论留言。

  1. 直接暴力求解,将20个数组合并,然后排序,取出500个数

  2. 使用归并。 对于排好序的 序列,我们要注意使用归并。
    先将第1个和第2个归并,得到500个数据(没必要归并得到总的1000个数)。然后再加结果和第3个归并,得到500个数据,再与第4个归并,等等。

  3. 利用堆。保持一个20的堆,然后先将每个数组的第1个数入堆。

    20个元素的堆一直保持容量为20个,20个数组的最小元素可以将20个数组的第0个元素入堆,最小堆的性质,顶点为最小值。这时候得到了500个结果里的第0个结果。然后再把下一个元素入20个元素的堆,堆插入的时候会保持性质不变,最小元素依然在顶点。再取出20个元素的顶点,得到500个结果里的第1个结果。

    假设 [1,3,4,5,6] [2,3,4,5,6] [3,4,5,6,7]
    最小的是比较 1 2 3 得到1
    次小的是将刚才的1替换为它后面的元素3, 再加上刚才的元素2 3,得到2

    注意这儿需要保持数来自于哪个数组,以及其在数组里的位置

实力代码

方法1

1
2
3
4
5
6
7
8
9
10
11
12
// 方法1,直接将20个数组合并,排序,然后取前500个
// 复杂度是 10000log(10000)
Integer[] allData = new Integer[20 * 500];
int a = 0;
for (int ii = 0; ii < rowSize; ii++) {
for (int i = 0; i < columnSize; i++) {
allData[a++] = data[ii][i];
}
}
Arrays.sort(allData);
Integer[] result1 = Arrays.copyOfRange(allData, 0, columnSize);
System.out.println("method 1 result: " + Arrays.toString(result1));

方法2

先定义一个 merge 函数,从两个数组里面得到前500个数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
*
* @param first
* 数组1
* @param second
* 数组2
* @param n
* 数组的长度,假定 first 和 second 的长度均为 n
* @return 返回一个长度为 n 的数组
*/
private static Integer[] merge(Integer[] first, Integer[] second, int n) {
// 构建一个数组,这是归并排序的缺点,需要额外空间
Integer[] temp = new Integer[n];
int f = 0;
int s = 0;
int i = 0;
while (i != n && f <= n && s <= n) {
if (first[f] < second[s]) {
temp[i++] = first[f++];
} else {
temp[i++] = second[s++];
}
}
return temp;
}

下来是总的归并代码

1
2
3
4
5
6
7
8
9
10
11
12
// 方法2,归并,先归并前两个数组,取前500个数据。将得到的结果再与第3个归并,重复
// 考虑一下,20个数组先两两归并,得到10个;10个再两两归并,得到5个。等等,直到最后1个
// 但这样归并的次数和从前往后两两归并的次数是一样的?所以还是 上述的从前往后好了
// 复杂度是 19*500
Integer[] result2 = data[0];
for (int i = 1; i < rowSize; i++) {
result2 = merge(result2, data[i], columnSize);
}
System.out.println("method 2 result: " + Arrays.toString(result2));
System.out.println("result1 equals result2: " + Arrays.equals(result1, result2));

方法3

定义一个最小堆,有插入和删除方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/**
*
* 最小堆和堆排序, 最小堆,顶点的元素是最小值, 根据《Java 语言程序设计 进阶篇》 p83 改写, 书上是最大堆. 堆排序
* 将元素都存入最小堆中,从最小堆里面每次取出顶点元素
*
* @author tomchen
*
* @param <E>
*/
class MinHeap<E extends Comparable> {
public static <E extends Comparable> void heapSort(E[] array) {
MinHeap<E> heap = new MinHeap<E>();
for (int i = 0; i < array.length; i++) {
heap.add(array[i]);
}
System.out.println("Debug: heap is " + heap);
for (int i = 0; i < array.length; i++) {
array[i] = heap.removeTop();
}
}
private ArrayList<E> data = new ArrayList<E>();
public MinHeap() {
}
/**
* 增加一个新元素,步骤是 1. 先把元素插入到 list 的末尾 2. 比较末尾元素和它的父元素,若小于,交换两者 3.
* 重复上述步骤,直到到顶点位置或者子元素大于父元素 4. 不一定要遍历堆所有的元素,达到堆的性质后会提前结束
*
* @param array
*/
public void add(E array) {
data.add(array);
int child = data.size() - 1;
int parent = (child - 1) / 2;
// 判断是否到达顶点
while (child > 0) {
// 父元素大于子元素,交换,保持父是小的
if (data.get(parent).compareTo(array) > 0) {
data.set(child, data.get(parent));
data.set(parent, array);
child = parent;
parent = (child - 1) / 2;
} else {
// 已经是最小堆了,无需再比较
break;
}
}
}
/**
* 删除顶点处的元素,步骤是: 1. 把末尾的元素复制到顶点处 2. 然后比较此时顶点的值和左右子树,保持最小堆的性质 3.
* 交换顶点和左右子树较小的值 4. 重复上述步骤,直到已经成了最小堆或者遍历完 5. 注意可能存在左子树存在,右子树不存在情况 6.
* 不一定要遍历堆所有的元素,达到堆的性质后会提前结束
*
* @return 返回被删除的元素
*/
public E removeTop() {
if (data.isEmpty())
return null;
E removed = data.get(0);
// 因为一直交换的是最后的元素,这儿将其保存
E last = data.get(data.size() - 1);
data.set(0, last);
data.remove(data.size() - 1);
int parent = 0;
int leftChild = parent * 2 + 1;
int rightChild = parent * 2 + 2;
while (leftChild <= data.size() - 1) {
int minIndex = leftChild;
// 右子树存在,判断左右子树哪个小,保存坐标
// 如果不存在,那么使用左子树的坐标
// 保存较小元素的坐标,可以省去考虑左右子树都存在,只有左存在的情况
if (rightChild <= data.size() - 1) {
if (data.get(rightChild).compareTo(data.get(leftChild)) < 0) {
minIndex = rightChild;
}
}
if (data.get(minIndex).compareTo(last) < 0) {
data.set(parent, data.get(minIndex));
data.set(minIndex, last);
parent = minIndex;
leftChild = parent * 2 + 1;
rightChild = parent * 2 + 2;
} else {
break; // 已经达到了最小堆的性质
}
}
return removed;
}
@Override
public String toString() {
return data.toString();
}
}

为了能够记录堆里的元素来自哪个数组,以及在数组里的位置,我们使用一个内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class DataWithSource implements Comparable<DataWithSource> {
// 数据
private Integer value;
// 来源的数组
private Integer comeFrom;
// 在数组中的 index
private Integer index;
public DataWithSource(Integer value, Integer comeFrom, Integer index) {
this.value = value;
this.comeFrom = comeFrom;
this.index = index;
}
public Integer getComeFrom() {
return comeFrom;
}
public Integer getValue() {
return value;
}
public void setValue(Integer value) {
this.value = value;
}
public Integer getIndex() {
return index;
}
public void setIndex(Integer index) {
this.index = index;
}
public int compareTo(DataWithSource o) {
return this.value.compareTo(o.value);
}
}

下面是方法3的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public static void main(String[] args) {
Random r = new Random();
int rowSize = 20;
int columnSize = 500;
// 注意 java 二维数组其实是一维数组,里面包含的也是一维数组
Integer[][] data = new Integer[rowSize][columnSize];
for (int ii = 0; ii < rowSize; ii++) {
for (int i = 0; i < columnSize; i++) {
data[ii][i] = r.nextInt(1600);
}
// 将500个元素排序,升序
Arrays.sort(data[ii]);
// System.out.println(Arrays.toString(data[ii]));
}
// 方法1,直接将20个数组合并,排序,然后取前500个
// 复杂度是 10000log(10000)
Integer[] allData = new Integer[20 * 500];
int a = 0;
for (int ii = 0; ii < rowSize; ii++) {
for (int i = 0; i < columnSize; i++) {
allData[a++] = data[ii][i];
}
}
Arrays.sort(allData);
Integer[] result1 = Arrays.copyOfRange(allData, 0, columnSize);
System.out.println("method 1 result: " + Arrays.toString(result1));
// 方法2,归并,先归并前两个数组,取前500个数据。将得到的结果再与第3个归并,重复
// 考虑一下,20个数组先两两归并,得到10个;10个再两两归并,得到5个。等等,直到最后1个
// 但这样归并的次数和从前往后两两归并的次数是一样的?所以还是 上述的从前往后好了
// 复杂度是 19*500
Integer[] result2 = data[0];
for (int i = 1; i < rowSize; i++) {
result2 = merge(result2, data[i], columnSize);
}
System.out.println("method 2 result: " + Arrays.toString(result2));
System.out.println("result1 equals result2: " + Arrays.equals(result1, result2));
// 方法3,保持一个最小堆,这个堆存放来自20个数组的最小数
// 每次取出一个数,然后将该数所在的数组的后面一个数入堆
// 重复上面步骤,取出500个数
// 注意建堆的时候需要保持 数来自哪个数组,用一个内部类实现
// 复杂度是 500 * log(20)
Integer[] result3 = new Integer[500];
MinHeap<DataWithSource> heap = new MinHeap<DataWithSource>();
for (int i = 0; i < rowSize; i++) {
// 记录下来源那个数组,以及在数组中的 index
DataWithSource d = new DataWithSource(data[i][0], i, 0);
heap.add(d);
}
int num = 0;
while (num < columnSize) {
// 删除顶点元素
DataWithSource d = heap.removeTop();
result3[num++] = d.getValue();
// 将 value 置为该数原数组里的下一个数
d.setValue(data[d.getComeFrom()][d.getIndex() + 1]);
// 将其在数组中的 index +1
d.setIndex(d.getIndex() + 1);
heap.add(d);
}
System.out.println("method 3 result: " + Arrays.toString(result3));
System.out.println("result2 equals result3: " + Arrays.equals(result2, result3));
}

输出

1
2
3
4
5
method 1 result: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 9, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 16, 17, 17, 18, 18, 18, 18, 19, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 33, 33, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 35, 35, 35, 36, 36, 36, 36, 36, 37, 37, 37, 37, 38, 38, 38, 38, 38, 38, 38, 39, 39, 39, 39, 39, 39, 39, 39, 40, 40, 40, 40, 41, 41, 41, 41, 41, 41, 41, 41, 42, 42, 42, 42, 42, 42, 42, 43, 43, 43, 43, 44, 44, 44, 44, 44, 45, 45, 45, 45, 45, 45, 45, 45, 45, 46, 46, 46, 46, 46, 46, 47, 47, 48, 48, 48, 49, 49, 49, 50, 50, 50, 50, 50, 51, 51, 51, 51, 51, 51, 51, 51, 52, 52, 52, 52, 52, 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 54, 54, 54, 54, 54, 54, 54, 54, 55, 55, 55, 55, 55, 55, 56, 56, 56, 56, 56, 56, 56, 57, 57, 57, 57, 57, 57, 57, 58, 58, 58, 58, 58, 58, 58, 58, 58, 59, 59, 59, 59, 59, 59, 59, 59, 60, 60, 60, 60, 60, 60, 61, 61, 61, 61, 61, 61, 61, 61, 61, 62, 62, 62, 62, 62, 62, 62, 62, 63, 63, 63, 63, 63, 63, 63, 64, 64, 64, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 66, 66, 66, 66, 66, 66, 66, 66, 66, 67, 67, 67, 68, 68, 68, 68, 69, 69, 69, 69, 69, 69, 69, 69, 69, 69, 69, 70, 70, 70, 70, 71, 71, 71, 71, 71, 71, 71, 72, 72, 72, 72, 72, 72, 72, 73, 73, 73, 73, 73, 74, 74, 74, 74, 74, 74, 74, 75, 75, 76, 76, 76, 76, 76, 76, 76, 76, 76, 77, 77, 77, 77, 77, 77, 78, 78, 78, 78, 78, 78, 79, 79, 79]
method 2 result: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 9, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 16, 17, 17, 18, 18, 18, 18, 19, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 33, 33, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 35, 35, 35, 36, 36, 36, 36, 36, 37, 37, 37, 37, 38, 38, 38, 38, 38, 38, 38, 39, 39, 39, 39, 39, 39, 39, 39, 40, 40, 40, 40, 41, 41, 41, 41, 41, 41, 41, 41, 42, 42, 42, 42, 42, 42, 42, 43, 43, 43, 43, 44, 44, 44, 44, 44, 45, 45, 45, 45, 45, 45, 45, 45, 45, 46, 46, 46, 46, 46, 46, 47, 47, 48, 48, 48, 49, 49, 49, 50, 50, 50, 50, 50, 51, 51, 51, 51, 51, 51, 51, 51, 52, 52, 52, 52, 52, 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 54, 54, 54, 54, 54, 54, 54, 54, 55, 55, 55, 55, 55, 55, 56, 56, 56, 56, 56, 56, 56, 57, 57, 57, 57, 57, 57, 57, 58, 58, 58, 58, 58, 58, 58, 58, 58, 59, 59, 59, 59, 59, 59, 59, 59, 60, 60, 60, 60, 60, 60, 61, 61, 61, 61, 61, 61, 61, 61, 61, 62, 62, 62, 62, 62, 62, 62, 62, 63, 63, 63, 63, 63, 63, 63, 64, 64, 64, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 66, 66, 66, 66, 66, 66, 66, 66, 66, 67, 67, 67, 68, 68, 68, 68, 69, 69, 69, 69, 69, 69, 69, 69, 69, 69, 69, 70, 70, 70, 70, 71, 71, 71, 71, 71, 71, 71, 72, 72, 72, 72, 72, 72, 72, 73, 73, 73, 73, 73, 74, 74, 74, 74, 74, 74, 74, 75, 75, 76, 76, 76, 76, 76, 76, 76, 76, 76, 77, 77, 77, 77, 77, 77, 78, 78, 78, 78, 78, 78, 79, 79, 79]
result1 equals result2: true
method 3 result: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 9, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 16, 17, 17, 18, 18, 18, 18, 19, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 33, 33, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 35, 35, 35, 36, 36, 36, 36, 36, 37, 37, 37, 37, 38, 38, 38, 38, 38, 38, 38, 39, 39, 39, 39, 39, 39, 39, 39, 40, 40, 40, 40, 41, 41, 41, 41, 41, 41, 41, 41, 42, 42, 42, 42, 42, 42, 42, 43, 43, 43, 43, 44, 44, 44, 44, 44, 45, 45, 45, 45, 45, 45, 45, 45, 45, 46, 46, 46, 46, 46, 46, 47, 47, 48, 48, 48, 49, 49, 49, 50, 50, 50, 50, 50, 51, 51, 51, 51, 51, 51, 51, 51, 52, 52, 52, 52, 52, 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 54, 54, 54, 54, 54, 54, 54, 54, 55, 55, 55, 55, 55, 55, 56, 56, 56, 56, 56, 56, 56, 57, 57, 57, 57, 57, 57, 57, 58, 58, 58, 58, 58, 58, 58, 58, 58, 59, 59, 59, 59, 59, 59, 59, 59, 60, 60, 60, 60, 60, 60, 61, 61, 61, 61, 61, 61, 61, 61, 61, 62, 62, 62, 62, 62, 62, 62, 62, 63, 63, 63, 63, 63, 63, 63, 64, 64, 64, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 66, 66, 66, 66, 66, 66, 66, 66, 66, 67, 67, 67, 68, 68, 68, 68, 69, 69, 69, 69, 69, 69, 69, 69, 69, 69, 69, 70, 70, 70, 70, 71, 71, 71, 71, 71, 71, 71, 72, 72, 72, 72, 72, 72, 72, 73, 73, 73, 73, 73, 74, 74, 74, 74, 74, 74, 74, 75, 75, 76, 76, 76, 76, 76, 76, 76, 76, 76, 77, 77, 77, 77, 77, 77, 78, 78, 78, 78, 78, 78, 79, 79, 79]
result2 equals result3: true

Android实现RecyclerView下拉刷新和上拉加载更多

需求

先上效果图, Material Design风格的下拉刷新和上拉加载更多。

blog.csdn.net/never_cxb

源码地址(欢迎star) https://github.com/studychen/SeeNewsV2

如果对于RecyclerView还不熟悉,参见这篇 Android Material Design学习之RecyclerView代替 ListView

本文链接 http://blog.csdn.net/never_cxb/article/details/50759109 转载请注明出处

下拉刷新

效果图

上拉时候会有一个圆形动画,刷新加载数据。

http://blog.csdn.net/never_cxb/

思路

使用Google官方的android.support.v4.widget.SwipeRefreshLayout

列表RecyclerView的xml布局

给原来的RecyclerView增加一个SwipeRefreshLayout的父布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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:background="?attr/listBackground"
android:orientation="vertical">
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swiperefreshlayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- 新闻列表展示-->
<android.support.v7.widget.RecyclerView
android:id="@+id/rcv_article_origin"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.v4.widget.SwipeRefreshLayout>
</LinearLayout>

java代码

这儿有几个注意点:
setColorSchemeColors()可以控制圆形动画的颜色,最多设置4个。

setOnRefreshListener 设置下拉刷新的回调事件。

下拉刷新后,使用 AsyncTask 根据当前RecyclerView中首个Item的id来加载更多数据。

数据加载完毕后,使用setRefreshing(false);取消动画。

如果刷新后得到0条记录,提示没有数据更新。若得到>0条数据,把数据加到RecyclerView中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
mSwipeRefreshLayout.setColorSchemeColors(Color.RED, Color.BLUE);
mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
new MoreArticleTask().execute(mAdapter.getTopArticleId());
}
});
// Integer 是输入参数
// 得到比某个id大的新闻数组
class MoreArticleTask extends AsyncTask<Integer, Void, List<SimpleArticleItem>> {
@Override
protected List<SimpleArticleItem> doInBackground(Integer... params) {
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getMoreById(mColumn, params[0]);
}
@Override
protected void onPostExecute(List<SimpleArticleItem> simpleArticleItems) {
super.onPostExecute(simpleArticleItems);
if (mSwipeRefreshLayout != null) {
mSwipeRefreshLayout.setRefreshing(false);
}
//没有新的数据,提示消息
if (simpleArticleItems == null || simpleArticleItems.size() == 0) {
Snackbar.with(mActivity.getApplicationContext()) // context
.text(mActivity.getResources().getString(R.string.list_more_data)) // text to display
.duration(Snackbar.SnackbarDuration.LENGTH_SHORT) // make it shorter
.show(mActivity); // activity where it is displayed
} else {
mArticleList.addAll(simpleArticleItems);
mAdapter.notifyDataSetChanged();
}
}
}

首次进入页面就显示加载ing的动画

效果如下:

http://blog.csdn.net/never_cxb/

直接使用 mSwipeRefreshLayout.setRefreshing(true);加载动画初始状态并不显示。

看了 http://stackoverflow.com/questions/26858692/swiperefreshlayout-setrefreshing-not-showing-indicator-initially 的解答,
改用下面的代码,初始状态就有加载动画显示。

1
2
3
4
5
6
7
mSwipeRefreshLayout.post(new Runnable() {
@Override
public void run() {
mSwipeRefreshLayout.setRefreshing(true);
new MoreArticleTask().execute(mAdapter.getTopArticleId());
}
});

上拉加载更多

RecyclerView 展示列表是个通用需求,那么当数据较多时候,如何实现分页加载呢?

比如笔者的项目中,先加载15条新闻,当滑动到底部时候,再加载15条。

效果图

http://blog.csdn.net/never_cxb/

思路

在RecyclerView底部增加一个Footer的ViewHolder,数据加载完毕取出底部的ViewHolder。

底部Footer的xml布局文件

很简单,就是一个ProgressBar。本项目为了Material Design,使用了一个开源ProgressBar https://github.com/Todd-Davies/ProgressWheel,你也可以使用原生的ProgressBar。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?><!--上拉加载更多 RecyclerView 底部-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:wheel="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.pnikosis.materialishprogress.ProgressWheel
android:id="@+id/rcv_load_more"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_horizontal"
wheel:matProg_barColor="@color/accent"
wheel:matProg_progressIndeterminate="true" />
</LinearLayout>

##底部Footer的ViewHolder

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 底部加载更多
*/
class FooterViewHolder extends RecyclerView.ViewHolder {
@InjectView(R.id.rcv_load_more)
ProgressWheel rcvLoadMore;
public FooterViewHolder(View itemView) {
super(itemView);
ButterKnife.inject(this, itemView);
}
}

在Fragment中给RecyclerView增加滑动监听

layoutManager.getItemCount() 可以得到当前RecyclerView中Item的总数目

layoutManager.findLastVisibleItemPosition() 得到最后一个可见的Item的位置position

如果 totalItemCount < (lastVisibleItem + Constant.VISIBLE_THRESHOLD)

比如一共15个Item,当前到达第13个, Constant.VISIBLE_THRESHOLD设为3
总数小于最后一个+阈值,就加载更多新闻数据,同时把loading标记为 true 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private boolean loading = false;
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mRecyclerView.setLayoutManager(new LinearLayoutManager(mActivity));
mAdapter = new OriginArticleAdapter(mActivity, mArticleList);
mRecyclerView.setAdapter(mAdapter);
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
int totalItemCount = layoutManager.getItemCount();
int lastVisibleItem = layoutManager.findLastVisibleItemPosition();
if (!loading && totalItemCount < (lastVisibleItem + Constant.VISIBLE_THRESHOLD)) {
new ArticleTask(mActivity).execute(mAdapter.getBottomArticleId());
loading = true;
}
}
});
}

在Fragment中控制底部Footer

我们依然使用AsyncTask。

注意 onPreExecute() 给 mArticleList 增加了一个null标记Footer,如果是第一次进入页面(mArticleList为空)不需要加Footer。

当数据加载完毕后,用 mArticleList.remove(mArticleList.size() - 1);把最下面的Footer删除。

再用 mArticleList.addAll(moreArticles); 增加新增的新闻数据,

并用 mAdapter.notifyDataSetChanged(); 通知 RecyclerView.Adapter 有数据改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
private List<SimpleArticleItem> mArticleList = new ArrayList<SimpleArticleItem>();
class ArticleTask extends AsyncTask<Integer, Void, List<SimpleArticleItem>> {
private Context mContext;
public ArticleTask(Context context) {
mContext = context;
}
/**
* Runs on the UI thread before {@link #doInBackground}.
*/
@Override
protected void onPreExecute() {
super.onPreExecute();
if (mArticleList != null && mArticleList.size() > 0) {
mArticleList.add(null);
// notifyItemInserted(int position),这个方法是在第position位置
// 被插入了一条数据的时候可以使用这个方法刷新,
// 注意这个方法调用后会有插入的动画,这个动画可以使用默认的,也可以自己定义。
mAdapter.notifyItemInserted(mArticleList.size() - 1);
}
}
/**
* @param params 偏移量 aid
* @return
*/
@Override
protected List<SimpleArticleItem> doInBackground(Integer... params) {
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getArticleList(mColumn, params[0]);
}
@Override
protected void onPostExecute(final List<SimpleArticleItem> moreArticles) {
// 新增新闻数据
super.onPostExecute(moreArticles);
if (mArticleList.size() == 0) {
mArticleList.addAll(moreArticles);
mAdapter.notifyDataSetChanged();
} else {
//删除 footer
mArticleList.remove(mArticleList.size() - 1);
mArticleList.addAll(moreArticles);
mAdapter.notifyDataSetChanged();
loading = false;
}
}
}

Override RecyclerView.Adapter 中的 getItemViewType

extends RecyclerView.Adapter<RecyclerView.ViewHolder> 如果是null,返回Footer的Type;否则,返回正常新闻的Type。

本文,为了UI美观,把新闻分为两种:大于3幅图片 、小于3幅图片,相应返回不同的Type和ViewHolder。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final static int TYPE_MULTI_IMAGES = 2; // 多个图片的文章
public final static int TYPE_FOOTER = 3;//底部--往往是loading_more
public final static int TYPE_NORMAL = 1; // 正常的一条文章
@Override
public int getItemViewType(int position) {
SimpleArticleItem article = articleList.get(position);
if (article == null) {
return TYPE_FOOTER;
} else if (article.getImageUrls().length >= 3) {
return TYPE_MULTI_IMAGES;
} else {
return TYPE_NORMAL;
}
}

Override RecyclerView.Adapter 中的 onCreateViewHolder

在该方法中利用 switch (viewType) 返回不同的xml布局文件及ViewHolder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
RecyclerView.ViewHolder vh;
View view;
switch (viewType) {
//其他无法处理的情况使用viewholder_article_simple
default:
case TYPE_NORMAL:
view = mLayoutInflater.inflate(
R.layout.item_article_normal, parent, false);
vh = new ItemArticleViewHolder(view);
return vh;
case TYPE_FOOTER:
view = mLayoutInflater.inflate(
R.layout.recyclerview_footer, parent, false);
vh = new FooterViewHolder(view);
return vh;
case TYPE_MULTI_IMAGES:
view = mLayoutInflater.inflate(
R.layout.item_article_multi_images, parent, false);
vh = new MultiImagesViewHolder(view);
return vh;
}
}

Override RecyclerView.Adapter 中的 onBindViewHolder

利用 instanceof 判断是何种类型的ViewHolder,来给控件赋值、绑定数据。

注意:因为Footer是用null表示的,为了防止NullPointerException,
我们先用if把FooterViewHolder的情况处理了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
//这时候 article是 null,先把 footer 处理了
if (holder instanceof FooterViewHolder) {
((FooterViewHolder) holder).rcvLoadMore.spin();
return;
}
SimpleArticleItem article = articleList.get(position);
String[] imageUrls = article.getImageUrls();
if (holder instanceof ItemArticleViewHolder) {
ItemArticleViewHolder newHolder = (ItemArticleViewHolder) holder;
newHolder.rcvArticleTitle.setText(article.getTitle());
newHolder.rcvArticleDate.setText(article.getPublishDate());
//当图片小于3张时候 选取第1张图片
if (imageUrls.length > 0) {
newHolder.rcvArticlePhoto.setImageURI(Uri.parse(Constant.BUCKET_HOST_NAME
+ imageUrls[0]));
} else {
newHolder.rcvArticlePhoto.setImageURI(Uri.parse(ApiUrl.randomImageUrl(article.getId())));
}
//注意这个阅读次数是 int 类型,需要转化为 String 类型
newHolder.rcvArticleReadtimes.setText("浏览: " + article.getReadTimes());
newHolder.rcvArticleSummary.setText(article.getSummary());
} else {
MultiImagesViewHolder newHolder = (MultiImagesViewHolder) holder;
newHolder.articleTitle.setText(article.getTitle());
newHolder.articlePic1.setImageURI(Uri.parse(Constant.BUCKET_HOST_NAME + imageUrls[0]));
newHolder.articlePic2.setImageURI(Uri.parse(Constant.BUCKET_HOST_NAME + imageUrls[1]));
newHolder.articlePic3.setImageURI(Uri.parse(Constant.BUCKET_HOST_NAME + imageUrls[2]));
newHolder.countPics.setText("图片: " + imageUrls.length);
newHolder.countRead.setText("浏览: " + article.getReadTimes());
}
}

下拉刷新和上拉加载更多的源码地址(欢迎star) https://github.com/studychen/SeeNewsV2

本文链接 http://blog.csdn.net/never_cxb/article/details/50759109 转载请注明出处

一些坑

注意给RecyclerView setLayoutManager,不然RecyclerView可能不显示

1
2
3
4
5
// 1. get a reference to recyclerView
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.list);
// 2. set layoutManger
recyclerView.setLayoutManager(new LinearLayoutManager(this));

参考文章

如何演示你的App? Android录制Gif动态图教程

需求

统计结果1

Android App开发完了,自然希望录个gif做个展示。视频也可以做展示,但是需要上传到优酷、土豆等等,没有gif轻量简单省流量。

本文教大家如何录制gif,链接 http://www.alijava.com/gif-record/,转载请注明出处。

思路

生成gif的思路是两步

  1. 把App操作过程录制成视频
  2. 根据视频转换成Gif

目前网上录制GIf的思路也基本都是分为这2步,不知道有没有更好的方法,一步就生成gif动态的?欢迎留言补充

利用adb 录制屏幕

在Android sdk下面有一些很有用的工具,adb位于platform-tools文件夹,开发者用它在设备上安装启动应用。早期的sdk版本中,adb位于tools文件夹中。

在终端(linux或者mac os)或者命令提示符(windows)键入

1
adb help all

可以列出所有可用的命令。

注意,如果经常使用adb工具,建立把sdk的文件夹路径添加到PATH环境变量中。不加入到环境变量中,每次启动adb都需要cd到platform-tools文件夹的位置。

我们录制屏幕利用 adb shell screenrecord命令,还可以使用adb shell screenshot进行截屏。下面是使用说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
tomchen$ ./adb shell screenrecord --help
Usage: screenrecord [options] <filename>
Records the device's display to a .mp4 file.
Options:
--size WIDTHxHEIGHT
Set the video size, e.g. "1280x720". Default is the device's main
display resolution (if supported), 1280x720 if not. For best results,
use a size supported by the AVC encoder.
--bit-rate RATE
Set the video bit rate, in megabits per second. Default 4Mbps.
--time-limit TIME
Set the maximum recording time, in seconds. Default / maximum is 180.
--rotate
Rotate the output 90 degrees.
--verbose
Display interesting information on stdout.
--help
Show this message.
Recording continues until Ctrl-C is hit or the time limit is reached.
tomchen$ pwd
/Applications/sdk/platform-tools

可以用--size指定视频分辨率的大小,--bit-rate指定比特率的大小。一般我们不需要设置,用默认的就行了。

1
tomchen$ ./adb shell screenrecord /sdcard/example.mp4

然后就可以录制的,默认时间是180s ,一般不需要这么长,录制完之后我们ctrl+c提前结束就行。
下面利用 pull 命令把手机上的视频拷到电脑上(也可以用手机助手啥的)

1
2
adb push <local> <remote> 将电脑上的文件复制到手机(通常是 sd 卡)
adb pull <remote> <local> 将手机上的文件复制到电脑

示例:

1
2
tomchen$ ./adb pull /sdcard/example.mp4 ~/Documents/
8786 KB/s (9449246 bytes in 1.050s)

Android studio 自带录制功能

现在一般都不要 Eclipse 开发 Android,转移到 Android Studio,录制屏幕的功能 google 自然想到了,点击开始按钮就行了(适合不熟悉命令行的同学们)
在 Android Studio 最下方的Android栏左边有一个按钮(下图红框圈出的部分),点击就可以实现录屏。还可以选择比特率、分辨率等,分辨率没空则采用默认值。

android

点击Start Recording就开始录制了,会弹出录制时间框

android

录完之后点击Stop Recording,停止录制。

android

最后会提示录制视频的保存位置,自己选个文件夹保存。

Android Studio 也提供了截屏功能,就在录制按钮的上方

android

点击截屏会弹出手机当前的操作界面,还可以用Reload刷新手机界面。

android

视频转 gif

这儿方法也有很多

  • 格式工厂之类的,输入视频格式,导出为gif格式
  • 截取很多帧图片,将多张图片拼接为gif
  • QQ影音工具箱自带了截取一段视频保存为gif格式

本文采用一种最简单的方法,利用一款叫 GifCam 的绿色版软件

Windows 版本的GifCam 可以到这儿下载,不需要下载分数。 http://download.csdn.net/detail/never_cxb/9404806

使用方法很简单,

  1. 用一个播放器打开咱们刚才录制好的mp4视频,然后拖动调整 GifCam 大小,让它的透明区域(录制gif区域)覆盖你要录制范围。
  2. 点击播放器播放视频,再点击GifCam的Rec按钮,就可以录制gif了。
  3. 录完点击stop按钮,选择gif文件保存位置。

android


本文讲解Android 下如何录制App操作生成Gif动态图,链接 http://www.alijava.com/gif-record/,转载请注明出处。

Java基于Jsoup爬虫给App提供数据

需求

最近基于 Material Design 重构了自己的新闻 App,数据来源是个问题。

有前人分析了知乎日报、凤凰新闻等 API,根据相应的 URL 可以获取新闻的 JSON 数据。为了锻炼写代码能力,笔者打算爬虫新闻页面,自己获取数据构建 API。

本文链接 http://blog.csdn.net/never_cxb/article/details/50524571 转载请注明出处

效果图

下图是原网站的页面

blog.csdn.net/never_cxb

爬虫获取了数据,展示到 APP 手机端

blog.csdn.net/never_cxb

爬虫思路

1
2
3
4
5
6
7
8
st=>start: 开始
e=>end: 结束
op1=>operation: 基于Get请求获取URL对于的网页Html
op2=>operation: 利用Jsoup把Html解析为Document
op3=>operation: 利用Dom的getElementsById等方法获取标题、发布时间、内容等
op4=>operation: 根据标题、发布时间、内容构建javabean给APP使用
st->op1->op2->op3->op4->e

关于 App 的实现过程可以参看这几篇文章,本文主要讲解一下如何爬虫数据。

Jsoup 简介

Jsoup 是一个 Java 的开源HTML解析器,可直接解析某个URL地址、HTML文本内容。

Jsoup主要有以下功能:

- 从一个URL,文件或字符串中解析HTML;

- 使用DOM或CSS选择器来查找、取出数据;

- 对HTML元素、属性、文本进行操作;

- 清除不受信任的HTML (来防止XSS攻击)

到官网下载相应的Jsoup依赖包 http://jsoup.org/download

Get 请求获取网页 HTML

新闻网页Html的DOM树如下所示:

http://blog.csdn.net/never_cxb

下面这段代码根据指定的 url,用代码获取get 请求返回的 html 源代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static String doGet(String urlStr) throws CommonException {
URL url;
String html = "";
try {
url = new URL(urlStr);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(5000);
connection.setDoInput(true);
connection.setDoOutput(true);
if (connection.getResponseCode() == 200) {
InputStream in = connection.getInputStream();
html = StreamTool.inToStringByByte(in);
} else {
throw new CommonException("新闻服务器返回值不为200");
}
} catch (Exception e) {
e.printStackTrace();
throw new CommonException("get请求失败");
}
return html;
}

InputStream in = connection.getInputStream();将得到输入流转化为字符串是个普遍需求,我们将其抽象出来,写一个工具方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class StreamTool {
public static String inToStringByByte(InputStream in) throws Exception {
ByteArrayOutputStream outStr = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
StringBuilder content = new StringBuilder();
while ((len = in.read(buffer)) != -1) {
content.append(new String(buffer, 0, len, "UTF-8"));
}
outStr.close();
return content.toString();
}
}

解析 HTML 获取标题

利用 google 浏览器的审查元素,找出新闻标题对于的html 代码:

1
2
3
4
5
6
7
<div id="article_title">
<h1>
<a href="http://see.xidian.edu.cn/html/news/7428.html">
关于举办《经典音乐作品欣赏与人文审美》讲座的通知
</a>
</h1>
</div>

我们需要从上面的 HTML 中找出id="article_title"的部分,使用 getElementById(String id) 方法

1
2
3
4
5
6
7
8
9
String htmlStr = HttpTool.doGet(urlStr);
// 将获取的网页 HTML 源代码转化为 Document
Document doc = Jsoup.parse(htmlStr);
Element articleEle = doc.getElementById("article");
// 标题
Element titleEle = articleEle.getElementById("article_title");
String titleStr = titleEle.text();

获取发布日期、信息来源

同样找出对于的 HTML 代码

1
2
3
4
5
6
7
8
9
10
11
<html>
<head></head>
<body>
<div id="article_detail">
<span> 2015-05-28 </span>
<span> 来源: </span>
<span> 浏览次数: <script language="JavaScript" src="http://see.xidian.edu.cn/index.php/news/click/id/7428">
</script> 477 </span>
</div>
</body>
</html>

思路也和上面类似,使用 getElementById(String id) 方法找出id="article_detail"为Element,再利用getElementsByTag获取span 部分。因为一共有3个<span> ... </span>,所以返回的是Elements而不是Element。

1
2
3
4
5
6
7
8
9
// article_detail包括了 2016-01-15 来源: 浏览次数:177
Element detailEle = articleEle.getElementById("article_detail");
Elements details = detailEle.getElementsByTag("span");
// 发布时间
String dateStr = details.get(0).text();
// 新闻来源
String sourceStr = details.get(1).text();

解析浏览次数

如果打印出上面的details.get(2).text(),只会得到

1
浏览次数:

没有浏览次数?为什么呢?

因为浏览次数是JavaScript 渲染出来的, Jsoup爬虫可能仅仅提取HTML内容,得不到动态渲染出的数据。

解决方法有两种

  • 在爬虫的时候,内置一个浏览器内核,执行js渲染页面后,再抓取。这方面对应的工具有Selenium、HtmlUnit或者PhantomJs。可以查看这篇文章 《抓取前端渲染的页面》 http://webmagic.io/docs/zh/posts/chx-cases/js-render-page.html

  • 所以分析JS请求,找到对应数据的请求url

如果你访问上面的 urlhttp://see.xidian.edu.cn/index.php/news/click/id/7428,会得到下面的结果

1
document.write(478)

这个478就是我们需要的浏览次数,我们对上面的url做get 请求,得到返回的字符串,利用正则找出其中的数字。

1
2
3
4
5
// 访问这个新闻页面,浏览次数会+1,次数是 JS 渲染的
String jsStr = HttpTool.doGet(COUNT_BASE_URL + currentPage);
int readTimes = Integer.parseInt(jsStr.replaceAll("\\D+", ""));
// 或者使用下面这个正则方法
// String readTimesStr = jsStr.replaceAll("[^0-9]", "");

解析新闻内容

笔者本来是获取新闻内容纯文字的形式,但后来发现 Android 端也可以显示 CSS 格式,所以后来内容保留了 HTML 格式。

1
2
3
4
5
6
Element contentEle = articleEle.getElementById("article_content");
// 新闻主体内容
String contentStr = contentEle.toString();
// 如果用 text()方法,新闻主体内容的 html 标签会丢失
// 为了在 Android 上用 WebView 显示 html,用toString()
// String contentStr = contentEle.text();

解析图片 Url

注意一个网页上大大小小的图片很多,为了只获取新闻正文中的内容,我们最好首先定位到新闻内容的Element,然后再利用getElementsByTag(“img”)筛选出图片。

1
2
3
4
5
6
7
8
9
10
11
12
Element contentEle = articleEle.getElementById("article_content");
// 新闻主体内容
String contentStr = contentEle.toString();
// 如果用 text()方法,新闻主体内容的 html 标签会丢失
// 为了在 Android 上用 WebView 显示 html,用toString()
// String contentStr = contentEle.text();
Elements images = contentEle.getElementsByTag("img");
String[] imageUrls = new String[images.size()];
for (int i = 0; i < imageUrls.length; i++) {
imageUrls[i] = images.get(i).attr("src");
}

新闻实体类 JavaBean

上面获取了新闻的标题、发布日期、阅读次数、新闻内容等等,我们自然需要构造一个 javabean,把获取的内容封装进实体类中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ArticleItem {
private int index;
private String[] imageUrls;
private String title;
private String publishDate;
private String source;
private int readTimes;
private String body;
public ArticleItem(int index, String[] imageUrls, String title, String publishDate, String source, int readTimes,
String body) {
this.index = index;
this.imageUrls = imageUrls;
this.title = title;
this.publishDate = publishDate;
this.source = source;
this.readTimes = readTimes;
this.body = body;
}
@Override
public String toString() {
return "ArticleItem [index=" + index + ",\n imageUrls=" + Arrays.toString(imageUrls) + ",\n title=" + title
+ ",\n publishDate=" + publishDate + ",\n source=" + source + ",\n readTimes=" + readTimes + ",\n body=" + body
+ "]";
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public static ArticleItem getNewsItem(int currentPage) throws CommonException {
// 根据后缀的数字,拼接新闻 url
String urlStr = ARTICLE_BASE_URL + currentPage + ".html";
String htmlStr = HttpTool.doGet(urlStr);
Document doc = Jsoup.parse(htmlStr);
Element articleEle = doc.getElementById("article");
// 标题
Element titleEle = articleEle.getElementById("article_title");
String titleStr = titleEle.text();
// article_detail包括了 2016-01-15 来源: 浏览次数:177
Element detailEle = articleEle.getElementById("article_detail");
Elements details = detailEle.getElementsByTag("span");
// 发布时间
String dateStr = details.get(0).text();
// 新闻来源
String sourceStr = details.get(1).text();
// 访问这个新闻页面,浏览次数会+1,次数是 JS 渲染的
String jsStr = HttpTool.doGet(COUNT_BASE_URL + currentPage);
int readTimes = Integer.parseInt(jsStr.replaceAll("\\D+", ""));
// 或者使用下面这个正则方法
// String readTimesStr = jsStr.replaceAll("[^0-9]", "");
Element contentEle = articleEle.getElementById("article_content");
// 新闻主体内容
String contentStr = contentEle.toString();
// 如果用 text()方法,新闻主体内容的 html 标签会丢失
// 为了在 Android 上用 WebView 显示 html,用toString()
// String contentStr = contentEle.text();
Elements images = contentEle.getElementsByTag("img");
String[] imageUrls = new String[images.size()];
for (int i = 0; i < imageUrls.length; i++) {
imageUrls[i] = images.get(i).attr("src");
}
return new ArticleItem(currentPage, imageUrls, titleStr, dateStr, sourceStr, readTimes, contentStr);
}
public static void main(String[] args) throws CommonException {
System.out.println(getNewsItem(7928));
}

输出信息

1
2
3
4
5
6
7
8
ArticleItem [index=7928,
imageUrls=[/uploads/image/20160114/20160114225911_34428.png],
title=电院2014级开展“让诚信之花开遍冬日校园”教育活动,
publishDate=2016-01-14,
source=来源: 电影新闻网,
readTimes=200,
body=<div id="article_content">
<p style="text-indent:2em;" align="justify"> <strong><span style="font-size:16px;line-height:1.5;">西电新闻网讯</span></strong><span style="font-size:16px;line-height:1.5;">&nbsp;(通讯员</span><strong><span style="font-size:16px;line-height:1.5;"> 丁彤 王朱丹</span></strong><span style="font-size:16px;line-height:1.5;">...)

展望

最近用 Material Design 重构了自己的新闻 App,新闻数据是利用 Jsoup 实现的。第1版爬虫是在手机端实现的(我承认这设计很不好,既费流量又增加客户端负担),后来在新浪云上实现了一个简单的 JSP ,过滤了原网页的图片、一级栏目等,只返回新闻标题、阅读次数、新闻内容等等。

本文链接 http://blog.csdn.net/never_cxb/article/details/50524571 转载请注明出处

后期的打算是把爬虫这步移到新浪云上,返回格式化的 JSON 数据给客户端使用。可能的话,图片使用七牛CDN(Content Delivery Network 内容分发网络),在云上利用 Mysql 数据库缓存新闻信息。

参考文章

Android程序员面试常见知识点

前言

根据笔者自己的阅读以及项目经验总结而言,不同于网上的copy来copy去。

转载请注明出处 http://www.baidujava.com/android-interview/

Handler

在子线程里面创建 Handler 对象会抛出异常Can't create handler inside thread that has not called Looper.prepare()

但是加上Looper.prepare();不会抛出异常, 这个因为 Handler 对应一个 Looper,一个 Looper 对应一个线程。

1
2
3
4
5
6
7
8
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
handler2 = new Handler();
Looper.loop();
}
}).start();

用 ThreadLocal 保存 Looper 对象
ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

通过Looper.prepare()来创建Looper对象(消息队列封装在Looper对象中),并且保存在sThreadLoal中,然后通过Looper.loop()来执行消息循环,这两步通常是成对出现的!!

xml解析

个人理解:
Dom把xml全部加载,比较耗内存。
Sax是事件驱动,但是不支持随机读取,不能只处理部分元素。
Pull是Sax的改进,可以提前结束读取,Sax不能提前结束。

基本的解析方式包含两种,一种是事件驱动的(代表SAX),另一种方式是基于文档结构(代表DOM)。其他的只不过语法不一样而已。

Dom 方式解析xml,比较简单,并可以访问兄弟元素,但是需要将整个xml文档加载到内存中,对于android设备来说,不推荐使用dom的方式解析xml。

1
2
3
4
5
6
7
8
// 接收一个xml的字符串来解析xml,Document代表整个xml文档
Document document = builder.parse(inputStream);
// 得到xml文档的根元素节点
Element personsElement = document.getDocumentElement();
// 得到标签为person的Node对象的集合NodeList
NodeList nodeList = personsElement.getElementsByTagName("person");
Dom解析加载整个xml文档的树形结构,可以随时访问兄弟节点,父节点等。Sax只能向前遍历(单遍解析),使它不能支持随机访问。

sax和pull都是基于事件驱动的xml解析器,在解析xml时并不会加载整个的xml文档,占用内存较少,因此在android开发中建议使用sax或者pull来解析xml文档。

SAX,全称Simple API for XML,既是一种接口,也是一种软件包。它是一种XML解析的替代方法。SAX不同于DOM解析,它逐行扫描文档,一边扫描一边解析。由于应用程序只是在读取数据时检查数据,因此不需要将数据存储在内存中,这对于大型文档的解析是个巨大优势。

继承 extends DefaultHandler 需要复写下面几个方法。这几个方法是回调方法,解析时,若发生事件(文档开头结尾 元素开头结尾)的话会被调用这些回调方法。

  • startDocument() and endDocument() – Method called at the start and end of an XML document.
  • startElement() and endElement() – Method called at the start and end of a document element.
  • characters() – Method called with the text contents in between the start and end tags of an XML document element.

pull和sax的区别

Pull解析器和SAX解析器虽有区别但也有相似性。他们的区别为:SAX解析器的工作方式是自动将事件推入注册的事件处理器进行处理,因此你不能控制事件的处理主动结束;

而Pull解析器的工作方式为允许你的应用程序代码主动从解析器中获取事件,正因为是主动获取事件,因此可以在满足了需要的条件后不再获取事件,结束解析。这是他们主要的区别。

Android 系统架构

  1. Linux 内核层,为 Android 设备提供了底层的驱动,如蓝牙驱动、照相机驱动等等
  2. 系统运行库层,这些曾通过一些 C/C++ 库为 Android 系统提供了主要的特性支持,如 SQlite 提宫数据库,Webkit 提高了浏览器内核
    需要注意的是这一层也有 Android 运行时候需要的核心库
    Android 运行时库包括了 Dalvik 虚拟机
  3. 应用框架层,这一层提供了编写 App 时候需要用到的 Api
  4. 应用层,所有安装在手机上的 App 都属于这一层,包括短信、QQ 等

四大组件

activity和service默认是运行在应用进程的主线程中,四大组件默认都是和activity运行在同一个主线程中的

  • Activity 活动
  • Service 服务
    Service的Oncreate()方法只有第一次执行,而OnStartCommand()每次启动服务都会调用
  • Broadcast 广播接收器
    最常用的广播就是android.provider.Telephony.SMS_RECEIVED
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// AndroidManifest.xml
<uses-permission android:name="android.permission.RECEIVE_SMS" />
...
<application ...>
<receiver android:name=".SMSReceiver">
<intent-filter>
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
</application>
// SMSReceiver.java
public class SMSReceiver extends BroadcastReceiver
{
@Override
public void onReceive(Context context, Intent intent) {
Log.i(TAG, "SMS received.");
....
}
}
  • ContentProvider 内容提供者

    注意如果自己定义一个ContentProvider暴露为其他 App 使用,通常用SQLite或者文件实现,也可以用 Json 或者 Xml 实现等等。

4大组件都需要在 xml 文件里注册,注意它们是平级的关系。

存储化技术

  • 文件存储 存储一些图片、视频、doc 等等,保密性不强
  • SharedPreference 多用于存储用户配置、密码等等
    也可在里面存储视频当前播放的位置,按home键返回后重新从当前位置读取
  • 数据库 Sqlite 存储一些格式复杂的数据,比如短信中 联系人和对应的短信记录

    文件的操作模式

  • MODE_APPEND 追加内容
  • MODE_PRIVATE 覆盖文件中的内容
  • MODE_WORLD_READABLE 已经在4.2废弃
  • MODE_WORLD_WRITEABLE 已经在4.2废弃

打开文件示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
FileOutputStream out = null;
BufferedWriter writer = null;
try {
out = openFileOutput("data", Context.MODE_PRIVATE);
writer = new BufferedWriter(new OutputStreamWriter(out));
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (writer != null)
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}

读取文件 代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
FileInputStream in = null;
BufferedReader reader = null;
StringBuilder content = null;
try {
in = openFileInput("data");
reader = new BufferedReader(new InputStreamReader(in));
String line = "";
if ((line = reader.readLine()) != null) {
content.append(line);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if(reader != null)
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}

TestUtils

1
2
String x = "test";
TextUtils.isEmpty(x);

这儿的 isEmpty 方法可以判断 x 为 null 或者长度为 0

SQlite

使用adb shell中的sqlite3打开数据库

getWritableDatabase VS getReadableDatabase

getWritableDatabasegetReadableDatabase 取得的实例对数据库进行读和写的功能,不是像字面意思上一个读写权限和只读权限

两者的区别在于

  • getWritableDatabase取得的实例是以读写的方式打开数据库,如果打开的数据库磁盘满了,此时只能读不能写,此时调用了getWritableDatabase的实例,那么将会发生错误(异常)
  • getReadableDatabase取得的实例是先调用getWritableDatabase以读写的方式打开数据库,如果数据库的磁盘满了,此时返回打开失败,继而用getReadableDatabase的实例以只读的方式去打开数据库

    intent-filter

    intent-filter 可以动态注册也可以静态注册
    下面是静态注册的代码,直接写在xml文件里
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <application>
    <activity name=""/>
    <receiver android:name=".MyBroadcastReceiver">
    <!-- intent过滤器,指定可以匹配哪些intent, 一般需要定义action 可以是自定义的也可是系统的 -->
    <intent-filter>
    <action android:name="com.app.bc.test"/>
    </intent-filter>
    </receiver>
    </application>
    //
    // java code 这儿的intent也叫隐式intent
    Intent intent = new Intent(“com.app.bc.test”);
    sendBroadcast(intent);//发送广播事件

动态注册

1
2
3
4
5
6
7
8
//生成一个BroadcastReceiver对象
SMSReceiver smsReceiver = new SMSReceiver();
//生成一个IntentFilter对象
IntentFilter filter = new IntentFilter();
filter.addAction(“android.provider.Telephony.SMS_RECEIVED”);
//将BroadcastReceiver对象注册到系统当中
//此处表示该接收器会处理短信事件
TestBC1Activity.this.registerReceiver(smsReceiver, filter);
  • 静态注册:在AndroidManifest.xml注册,android不能自动销毁广播接收器,也就是说当应用程序关闭后,还是会接收广播。
  • 动态注册:在代码中通过registerReceiver()手工注册.当程序关闭时,该接收器也会随之销毁。当然,也可手工调用unregisterReceiver()进行销毁。

显示intent 和隐式intent

显示

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

隐式,它会自动寻找能处理 intent-filter 里面设置了处理该的 action 的Activity。

1
2
3
Intent intent = new Intent();
intent.setAction(“android.provider.Telephony.SMS_RECEIVED”);
startActivity(intent);

比如有手机装了多个浏览器,使用隐式intent会弹出让用户选择哪个浏览器。

Timer 和 Alarm

Timer 不适用需要长期在后台运行的定时任务,长时间不操作,CPU会睡眠状态,这会影响Timer的定时任务无法执行
Alarm 机制具有唤醒CPU的功能

工具方法

訪問Url,利用get請求,請返回的數據轉化為字符串

1
2
3
4
5
6
7
8
9
10
HttpURLConnection connection = null;
...
connection.setReadTimeOut(8000);
InputStream in = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
StringBuilder response = new StringBuilder();
String line = "";
while( (line = reader.readLine()) !=null ) {
response.append(line);
}

怎么更新UI

注意不能使用子线程更新 UI,可以用 Handler 或者 Activity.runOnUiThread

public final void runOnUiThread (Runnable action)

Added in API level 1
Runs the specified action on the UI thread. If the current thread is the UI thread, then the action is executed immediately. If the current thread is not the UI thread, the action is posted to the event queue of the UI thread.

Parameters
action    the action to run on the UI thread

Adapter 4个重写方法

BaseAdapter基本结构

  • public int getCount() :适配器中数据集中 数据的个数,即ListView需要显示的数据个数
  • public Object getItem(int position) : 获取数据集中与指定索引对应的数据项
  • public long getItemId(int position) : 获取指定行对应的ID
  • public View getView(int position, View convertView, ViewGroup parent) :获取每一个Item的显示内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class MyBaseAdapter extends BaseAdapter {
ArrayList myList = new ArrayList();
LayoutInflater inflater;
Context context;
public MyBaseAdapter(Context context, ArrayList myList) {
this.myList = myList;
this.context = context;
inflater = LayoutInflater.from(this.context);
}
@Override
public int getCount() {
return myList.size();
}
@Override
public ListData getItem(int position) {
return myList.get(position);
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
MyViewHolder mViewHolder;
if (convertView == null) {
convertView = inflater.inflate(R.layout.layout_list_item, parent, false);
mViewHolder = new MyViewHolder(convertView);
convertView.setTag(mViewHolder);
} else {
mViewHolder = (MyViewHolder) convertView.getTag();
}
ListData currentListData = getItem(position);
mViewHolder.tvTitle.setText(currentListData.getTitle());
mViewHolder.tvDesc.setText(currentListData.getDescription());
mViewHolder.ivIcon.setImageResource(currentListData.getImgResId());
return convertView;
}
private class MyViewHolder {
TextView tvTitle, tvDesc;
ImageView ivIcon;
public MyViewHolder(View item) {
tvTitle = (TextView) item.findViewById(R.id.tvTitle);
tvDesc = (TextView) item.findViewById(R.id.tvDesc);
ivIcon = (ImageView) item.findViewById(R.id.ivIcon);
}
}
}

在使用BaseAdapter时,我们需要自己创建一个类继承BaseAdapter,然后Eclipse会提醒我们实现上述四个方法,当给ListView设置了我们自己写的Adapter后,ListView内部会调用上述四个方法。

参考文章

【Android】BroadCast广播机制应用与实例

ListView using BaseAdapter – Android http://www.pcsalt.com/android/listview-using-baseadapter-android/

Android 新闻客户端基于 ViewPager 实现轮播图片

前言

新闻 App 首页最上方一般会循环播放热点图片,如下图所示。

http://blog.csdn.net/never_cxb

本文主要介绍了利用 ViewPager 实现轮播图片,图片下方加上小圆点指示器标记当前位置,并利用 Timer+Handler 实现了自动轮播播放。

本文链接 http://www.alijava.com/viewpager-photos/ 转载请注明出处

ViewPager展示图片

xml 布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?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">
<!--ViewPager 热门文章图片展示-->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@color/gray_light">
<android.support.v4.view.ViewPager
android:id="@+id/vp_hottest"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary" />
<LinearLayout
android:id="@+id/ll_hottest_indicator"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_gravity="bottom|right"
android:layout_marginBottom="5dp"
android:layout_marginRight="10dp"
android:layout_marginTop="5dp"
android:gravity="center"
android:orientation="horizontal" />
</FrameLayout>
</LinearLayout>

FrameLayout里面包含了ViewPager和LinearLayout,ViewPager 显示图片,LinearLayout是小圆点指示器区域,标记现在滑到哪张图片。

查看 xml 预览图,由于没有图片内容,当前只显示出红色矩形区域。

http://blog.csdn.net/never_cxb

新建javabean

首页的图片地址是新闻的一个属性,我们新建一个ItemArticle类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class ItemArticle {
// 新闻的 id
private int index;
// 新闻里的图片 url
private String imageUrl;
public ItemArticle(int index, String imageUrl) {
this.index = index;
this.imageUrl = imageUrl;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
public String getImageUrl() {
return imageUrl;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
}

适配器 PagerAdapter

继承自 android.support.v4.view.PagerAdapter,复写4个方法

  • instantiateItem(ViewGroup, int)
  • destroyItem(ViewGroup, int, Object)
  • getCount()
  • isViewFromObject(View, Object)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class HeaderAdapter extends PagerAdapter {
private static final String LOG = "NEWS_LOG";
private Activity context;
private List<ItemArticle> articles;
private List<SimpleDraweeView> images = new ArrayList<SimpleDraweeView>();
public HeaderAdapter(Activity context, List<ItemArticle> articles) {
this.context = context;
if (articles == null || articles.size() == 0) {
this.articles = new ArrayList<>();
} else {
this.articles = articles;
}
for (int i = 0; i < articles.size(); i++) {
SimpleDraweeView image = new SimpleDraweeView(context);
Uri uri = Uri.parse(articles.get(i).getImageUrl());
image.setImageURI(uri);
images.add(image);
}
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
container.addView(images.get(position));
return images.get(position);
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView(images.get(position));
}
@Override
public int getCount() {
return articles.size();
}
@Override
public boolean isViewFromObject(View view, Object object) {
Log.i(LOG, "in isViewFromObject view: " + view + " object: "
+ object + " equal: " + (view == (View) object));
return view == (View) object;
}
}

深入解析 isViewFromObject 方法

isViewFromObject(View view, Object object)的通用写法是return view == (View) object;
其中(View) object可根据具体情形替换成LinearLayout等等。

查看 ViewPager 源代码(戳这里

isViewFromObject是在infoForChild里被调用的,而且在该方法内会被调用mItems.size()次,mItems.size()是 ViewPager 里面图片的个数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static class ItemInfo {
Object object;
int position;
boolean scrolling;
}
private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>();
ItemInfo infoForChild(View child) {
for (int i=0; i<mItems.size(); i++) {
ItemInfo ii = mItems.get(i);
if (mAdapter.isViewFromObject(child, ii.object)) {
return ii;
}
}
return null;
}

ViewPager里面用了一个mItems 存储每个page的信息(ItemInfo),当界面要展示或者发生变化时,需要依据page的当前信息来调整,但此时只能通过view来查找,遍历mItems通过比较view和object来找到对应的ItemInfo。

1
2
Log.i(LOG, "in isViewFromObject view: " + view + " object: "
+ object + " equal: " + (view == (View) object));

所以我们如果打印出 Log 的话,会看到isViewFromObject()被调用多次,只有1次返回 true (表示找到了对应的ItemInfo),其他返回 false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
01-17 10:15:21.207 I/NEWS_LOG﹕ in isViewFromObject
view: SimpleDraweeView{holder=DraweeHolder{...}
object: SimpleDraweeView{holder=DraweeHolder{...}
equal: false
01-17 10:15:21.207 I/NEWS_LOG﹕ in isViewFromObject
view: SimpleDraweeView{holder=DraweeHolder{...}
object: SimpleDraweeView{holder=DraweeHolder{...}
equal: false
01-17 10:15:21.207 I/NEWS_LOG﹕ in isViewFromObject
view: SimpleDraweeView{holder=DraweeHolder{...}
object: SimpleDraweeView{holder=DraweeHolder{...}
equal: false
01-17 10:15:21.207 I/NEWS_LOG﹕ in isViewFromObject
view: SimpleDraweeView{holder=DraweeHolder{...}
object: SimpleDraweeView{....}}
equal: true

底部小圆点指示器

图片增加小圆点

轮播图片的底部都会加上小圆点,指示当前访问图片的位置。

http://blog.csdn.net/never_cxb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private ImageView[] mBottomImages;//底部只是当前页面的小圆点
//创建底部指示位置的导航栏
mBottomImages = new ImageView[headerArticles.size()];
for (int i = 0; i < mBottomImages.length; i++) {
ImageView imageView = new ImageView(mAct);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(10, 10);
params.setMargins(5, 0, 5, 0);
imageView.setLayoutParams(params);
if (i == 0) {
imageView.setBackgroundResource(R.drawable.indicator_select);
} else {
imageView.setBackgroundResource(R.drawable.indicator_not_select);
}
mBottomImages[i] = imageView;
//把指示作用的原点图片加入底部的视图中
llHottestIndicator.addView(mBottomImages[i]);
}

上面这段代码是小圆点的初始步骤,最开始是第0张图片被选中,所以是第0张小圆点是蓝色,其他小圆点是灰色。

addOnPageChangeListener小圆点动态变化

切换图片的时候,小圆点也要随着改变,这需要利用ViewPager.OnPageChangeListener,主要是下面这个方法:

public abstract void onPageSelected (int position)

This method will be invoked when a new page becomes selected. Animation is not necessarily complete.

Parameters
position    Position index of the new selected page.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
vpHottest.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
//图片左右滑动时候,将当前页的圆点图片设为选中状态
@Override
public void onPageSelected(int position) {
// 一定几个图片,几个圆点,但注意是从0开始的
int total = mBottomImages.length;
for (int j = 0; j < total; j++) {
if (j == position) {
mBottomImages[j].setBackgroundResource(R.drawable.indicator_select);
} else {
mBottomImages[j].setBackgroundResource(R.drawable.indicator_not_select);
}
}
}
@Override
public void onPageScrolled(int i, float v, int i1) {
}
@Override
public void onPageScrollStateChanged(int state) {
}
});

onPageSelected()中,利用 for 循环,将当前选中位置对应的小圆点置为蓝色,其他小圆点置为灰色。

自动播放

先定义一个 Handler,在主线程里面更新 UI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//定时轮播图片,需要在主线程里面修改 UI
private Handler mHandler = new Handler() {
public void handleMessage(Message msg) {
switch (msg.what) {
case UPTATE_VIEWPAGER:
if (msg.arg1 != 0) {
vpHottest.setCurrentItem(msg.arg1);
} else {
//false 当从末页调到首页是,不显示翻页动画效果,
vpHottest.setCurrentItem(msg.arg1, false);
}
break;
}
}
};

利用 Timer 实现每隔 5s 向 Handler 发送message来更新图片

1
2
3
4
5
6
7
8
9
10
11
12
13
// 设置自动轮播图片,5s后执行,周期是5s
timer.schedule(new TimerTask() {
@Override
public void run() {
Message message = new Message();
message.what = UPTATE_VIEWPAGER;
if (autoCurrIndex == headerArticles.size() - 1) {
autoCurrIndex = -1;
}
message.arg1 = autoCurrIndex + 1;
mHandler.sendMessage(message);
}
}, 5000, 5000);

为了使得滑到最后一页后能滑到首页,我们对于autoCurrIndex == headerArticles.size() - 1进行了处理。

一些知识点

完整代码

基于上面的分析,我们实现了自动轮播图片

http://blog.csdn.net/never_cxb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
public class MasterArticleFragment extends Fragment {
private static final String ARTICLE_LATEST_PARAM = "param";
private static final int UPTATE_VIEWPAGER = 0;
//轮播的最热新闻图片
@InjectView(R.id.vp_hottest)
ViewPager vpHottest;
//轮播图片下面的小圆点
@InjectView(R.id.ll_hottest_indicator)
LinearLayout llHottestIndicator;
//存储的参数
private String mParam;
//获取 fragment 依赖的 Activity,方便使用 Context
private Activity mAct;
//设置当前 第几个图片 被选中
private int autoCurrIndex = 0;
private ImageView[] mBottomImages;//底部只是当前页面的小圆点
private Timer timer = new Timer(); //为了方便取消定时轮播,将 Timer 设为全局
//定时轮播图片,需要在主线程里面修改 UI
private Handler mHandler = new Handler() {
public void handleMessage(Message msg) {
switch (msg.what) {
case UPTATE_VIEWPAGER:
if (msg.arg1 != 0) {
vpHottest.setCurrentItem(msg.arg1);
} else {
//false 当从末页调到首页是,不显示翻页动画效果,
vpHottest.setCurrentItem(msg.arg1, false);
}
break;
}
}
};
public static MasterArticleFragment newInstance(String param) {
MasterArticleFragment fragment = new MasterArticleFragment();
Bundle args = new Bundle();
args.putString(ARTICLE_LATEST_PARAM, param);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
mParam = savedInstanceState.getString(ARTICLE_LATEST_PARAM);
}
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_one_master, container, false);
mAct = getActivity();
ButterKnife.inject(this, view);
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
new ImageTask().execute();
}
@Override
public void onDestroyView() {
super.onDestroyView();
ButterKnife.reset(this);
}
private void setUpViewPager(final List<ItemArticle> headerArticles) {
HeaderAdapter imageAdapter = new HeaderAdapter(mAct, headerArticles);
vpHottest.setAdapter(imageAdapter);
//创建底部指示位置的导航栏
mBottomImages = new ImageView[headerArticles.size()];
for (int i = 0; i < mBottomImages.length; i++) {
ImageView imageView = new ImageView(mAct);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(10, 10);
params.setMargins(5, 0, 5, 0);
imageView.setLayoutParams(params);
if (i == 0) {
imageView.setBackgroundResource(R.drawable.indicator_select);
} else {
imageView.setBackgroundResource(R.drawable.indicator_not_select);
}
mBottomImages[i] = imageView;
//把指示作用的原点图片加入底部的视图中
llHottestIndicator.addView(mBottomImages[i]);
}
vpHottest.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
//图片左右滑动时候,将当前页的圆点图片设为选中状态
@Override
public void onPageSelected(int position) {
// 一定几个图片,几个圆点,但注意是从0开始的
int total = mBottomImages.length;
for (int j = 0; j < total; j++) {
if (j == position) {
mBottomImages[j].setBackgroundResource(R.drawable.indicator_select);
} else {
mBottomImages[j].setBackgroundResource(R.drawable.indicator_not_select);
}
}
//设置全局变量,currentIndex为选中图标的 index
autoCurrIndex = position;
}
@Override
public void onPageScrolled(int i, float v, int i1) {
}
@Override
public void onPageScrollStateChanged(int state) {
}
}
);
// 设置自动轮播图片,5s后执行,周期是5s
timer.schedule(new TimerTask() {
@Override
public void run() {
Message message = new Message();
message.what = UPTATE_VIEWPAGER;
if (autoCurrIndex == headerArticles.size() - 1) {
autoCurrIndex = -1;
}
message.arg1 = autoCurrIndex + 1;
mHandler.sendMessage(message);
}
}, 5000, 5000);
}
class ImageTask extends AsyncTask<String, Void, List<ItemArticle>> {
@Override
protected List<ItemArticle> doInBackground(String... params) {
List<ItemArticle> articles = new ArrayList<ItemArticle>();
articles.add(
new ItemArticle(1123, "http://***20151231105648_11790.jpg"));
articles.add(
new ItemArticle(1123, "http://***20151230152544_36663.jpg"));
articles.add(
new ItemArticle(1123, "http://***20151229204329_75030.jpg"));
articles.add(
new ItemArticle(1123, "http://***20151221151031_36136.jpg"));
return articles;
}
@Override
protected void onPostExecute(List<ItemArticle> articles) {
//这儿的 是 url 的集合
super.onPostExecute(articles);
setUpViewPager(articles);
}
}
}

schedule和scheduleAtFixedRate方法

(1)schedule方法:下一次执行时间相对于 上一次 实际执行完成的时间点 ,因此执行时间会不断延后。保持间隔时间的稳定
(2)scheduleAtFixedRate方法:下一次执行时间相对于上一次开始的 时间点 ,因此执行时间不会延后,存在并发性 。保持执行频率的稳定。

参考文章

Android实现Material Design风格的设置页面(滑动开关控件)

前言

本文链接 http://www.alijava.com/material-setting/ 转载请注明出处

参考了这篇文章 Material Design 风格的设置页面

笔者对原文章做了3个改进:

  • 把勾选框 改成了 Switch 的滑动开关,Material Design 更彻底

  • 替换后的 SwitchCompat 与整个 Preference 点击事件联动,保存到SharedPreferences

  • 在页面上方增加了 ToolBar,更贴近真实项目场景

blog.csdn.net/never_cxb

项目源代码地址(欢迎star) https://github.com/studychen/SeeNewsV2

基础:PreferenceScreen和PreferenceCategory

新建 res/xml/preferences.xml

Note: 是 xml 文件夹,不是 layout 文件夹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!-- res/xml/preferences.xml -->
<?xml version="1.0" encoding="utf-8"?><!--最新栏目的新闻-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/preference_item"
android:title="@string/title_activity_setting">
<PreferenceCategory
android:layout="@layout/preference_category_widget"
android:title="基本设置">
<CheckBoxPreference
android:key="@string/save_net_mode"
android:layout="@layout/preference_item"
android:summary="仅在Wi-Fi环境下才自动加载图片"
android:title="省流量模式"/>
<Preference
android:layout="@layout/preference_item"
android:summary="删除已缓存的文章内容及图片"
android:title="清空缓存" />
</PreferenceCategory>
<PreferenceCategory
android:layout="@layout/preference_category_widget"
android:title="其他说明">
<Preference
android:layout="@layout/preference_item"
android:summary="V 1.0"
android:title="当前版本" />
<Preference
android:layout="@layout/preference_item"
android:summary="博客:http://blog.csdn.net/never_cxb"
android:title="TomChen" />
</PreferenceCategory>
</PreferenceScreen>

android:layout 实现 Material Design 布局

上面

1
2
3
4
5
<PreferenceCategory
android:layout="@layout/preference_category_widget"
android:title="基本设置">
<!-- ... -->
</PreferenceCategory>

使用 android:layout="@layout/preference_category_widget" 改变 PreferenceCategory 布局

Note: 一定要使用系统的 id android:id="@android:id/title"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- preference_category_widget.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:orientation="vertical">
<TextView
android:id="@android:id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="8dp"
android:paddingTop="16dp"
android:textColor="@color/primary"
android:text="indroduce"
android:textSize="14sp" />
</LinearLayout>

preference_item.xml 定制 CheckBoxPreference 布局,勾选框或者滑动开关的布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!-- preference_item.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="?android:listPreferredItemHeight"
android:orientation="horizontal"
android:padding="16dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1">
<TextView
android:id="@android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:singleLine="true"
android:text="title"
android:textSize="16sp" />
<TextView
android:id="@android:id/summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@android:id/title"
android:text="summary"
android:textColor="#AAAAAA"
android:textSize="14sp" />
</RelativeLayout>
<LinearLayout
android:id="@android:id/widget_frame"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right|center_vertical"
android:orientation="vertical"/>
</LinearLayout>

在PreferenceFragment载入设置布局文件

1
2
3
4
5
6
7
public class SettingFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
}
}

这样就实现了原文里的效果图:

blog.csdn.net/never_cxb

CheckBox改为开关控件SwitchCompat

修改 CheckBoxPreference

使用 android:widgetLayout 帮助我们修改CheckBoxPreference布局。

建立 layout/switch_layout.xml 文件

1
2
3
4
5
6
7
<!-- switch_layout.xml -->
<!--此处代码有bug,下面会说明如何修正-->
<android.support.v7.widget.SwitchCompat xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textOff="OFF"
android:textOn="ON" />

在原来的CheckBoxPreference加上android:widgetLayout="@layout/switch_layout"

1
2
3
4
5
6
7
<!-- res/xml/preferences.xml -->
<CheckBoxPreference
android:key="@string/save_net_mode"
android:layout="@layout/preference_item"
android:summary="仅在Wi-Fi环境下才自动加载图片"
android:title="省流量模式"
android:widgetLayout="@layout/switch_layout" />

把控件是否选中保存到SharedPreferences中

1
2
<!-- res/values/strings.xml -->
<string name="save_net_mode">save_net_mode</string>

设置 android:key="@string/save_net_mode"属性,这是为了后续获取CheckBoxPreference

保存到 SharedPreferences 为了方便也使用这个字符串 save_net_mode

Java代码中用getPreferenceManager().findPreference("key的名称")来获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
final CheckBoxPreference checkboxPref = (CheckBoxPreference) getPreferenceManager()
.findPreference(getString(R.string.save_net_mode));
checkboxPref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
/**
* @param preference The changed Preference.
* @param newValue The new value of the Preference.
* @return True to update the state of the Preference with the new value.
*/
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
boolean checked = Boolean.valueOf(newValue.toString());
//保存到SharedPreferences中
PrefUtils.setSaveNetMode(checked);
Logger.d("Pref " + preference.getKey() + " changed to " + newValue.toString());
return true;
}
});

onPreferenceChange 没有调用

在代码加上了Log输出,但是点击开关控件,却没有反应。

笔者把android:widgetLayout="@layout/switch_layout"去掉
使用刚才的CheckBox,点击勾选或者取消勾选,发现可以保存到SharedPreferences

1
2
D/LoggerTag﹕ ║ Pref save_net_mode changed to false
D/LoggerTag﹕ ║ Pref save_net_mode changed to true

修改xml的SwitchCompat布局

增加 android:id="@android:id/checkbox"

增加

1
2
3
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false"

变成如下代码

1
2
3
4
5
6
7
8
9
10
<!-- switch_layout.xml -->
<android.support.v7.widget.SwitchCompat xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false"
android:textOff="OFF"
android:textOn="ON" />

这样点击开关按钮,按钮开或者关,会将 truefalse 保存到 SharedPreferences

增加ToolBar

新增 layout/setting.xml

res/xml/preferences.xml中增加ToolBar是不可能的
因为它的父节点是PreferenceScreen

我们新建一个 layout/setting.xml
在这个里面使用 Toolbar,下面的 FrameLayout 展示刚才的设置界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- setting.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<!--Toolbar-->
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar_preference"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize" />
<FrameLayout
android:id="@+id/content_frame"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

标题设置

1
getFragmentManager().beginTransaction().replace(R.id.content_frame, new SettingFragment()).commit();

把上面的 FrameLayout 布局区域设为 SettingFragment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class SettingActivity extends BaseActivity {
@InjectView(R.id.toolbar_preference)
Toolbar mToolbar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_setting);
ButterKnife.inject(this);
initToolbar();
getFragmentManager().beginTransaction().replace(R.id.content_frame, new SettingFragment()).commit();
}
/**
* 初始化Toolbar
*/
private void initToolbar() {
mToolbar.setTitle(getResources().getString(R.string.title_activity_setting));
mToolbar.setTitleTextColor(getResources().getColor(R.color.white));
setSupportActionBar(mToolbar);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setHomeAsUpIndicator(R.drawable.ic_left_back);
actionBar.setDisplayHomeAsUpEnabled(true);
}
}
/**
* 选项菜单
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
}
return false;
}
}

通过 actionBar.setHomeAsUpIndicator(R.drawable.ic_left_back);
增加了一个返回的白色箭头

通过 android.R.id.home获取白色箭头的点击事件,返回上一个Activity

本文链接 http://www.alijava.com/material-setting/ 转载请注明出处

项目源代码地址(欢迎star) https://github.com/studychen/SeeNewsV2

参考文章