Android 長截屏原理


https://android-notes.github.io/2016/12/03/android%E9%95%BF%E6%88%AA%E5%B1%8F%E5%8E%9F%E7%90%86/   android長截屏原理

 

小米系統自帶的長截屏應該很多人都用過,效果不錯。當長截屏時listview就會自動滾動,當按下停止截屏時,就會得到一張完整的截屏。

該篇就介紹一下長截屏的原理

上篇中介紹了android屏幕共享實現方式,該篇的原理和上一篇基本一致。

獲取view影像

當我們想得到一個view的影像時,我們可以調用系統api,得到view的bitmap,但有時可能得不到。我們可以通過另一種方式得到。

首先創建一個和view一樣大小的bitmap

1
Bitmap bmp = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.RGB_565);

然后把view繪制到bmp上

1
2
3
4
5
6
 
Canvas canvas = new Canvas();
 
canvas.setBitmap(bmp);
 
view.draw(canvas);

執行完上面代碼后bmp上就是view的影像了。

制造滾動事件,促使view滾動

我們可以創建一個MotionEvent,然后定時修改MotionEvent的y值,並分發給view,從而促使view上下滾動。當然我們也可以定時修改x值促使view左右滾動。

代碼大致如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 
final MotionEvent motionEvent = MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, view.getWidth() / 2, view.getHeight() / 2, 0);
 
view.postDelayed( new Runnable() {
@Override
public void run() {
 
motionEvent.setAction(MotionEvent.ACTION_MOVE);
 
motionEvent.setLocation(( int) motionEvent.getX(), (int) motionEvent.getY() - 1);
//把事件分發給view
view.dispatchTouchEvent(motionEvent);
 
view.postDelayed( this, DELAY);
}
}, DELAY);

注意:從分發DOWN事件到結束都要使用同一個MotionEvent對象,只需要不斷改變x或y值。

每次x或y的值相對於上次改動不能過大,若過大,view實際滾動距離可能達不到為MotionEvent設置的值(因view滾動時卡頓導致)。

截屏

當為MotionEvent設置的x或y值正好時當前view的大小時,創建新的bitmap,通過上述方法把view繪制到bitmap上,想要停止截屏時拼接所有bitmap即可。

備注

當我們想要把Listview長截屏時,需要為ListView外面嵌套一層和ListView一樣大小的View,以上的所有操作都在嵌套的這層view上操作。當我們調用嵌套的這層view的draw(new Canvas(bmp))時會把當前看到的這塊ListView繪制到bmp上,不管ListView嵌套了多少層子view都可以繪制到當前bmp上。

由於ListView中根據滑動的距離是否大於ViewConfiguration.get(view.getContext()).getScaledTouchSlop() )來確定要不要滾動,所以一開始我們要特殊處理下,為什么是ViewConfiguration.get(view.getContext()).getScaledTouchSlop() )可以查看ListView的事件分發相關函數得到(dispatchTouchEvent),讓Listview認為是開始滾動,這樣才能保證以后分發的滑動距離和實際滾動距離一致。

Listview也要通知是否滾動到了最后,不然如果沒有手動停止的話,雖然還是在一直分發滾動事件,但ListView不再滾動,導致最終截圖后后面全是重復的最后一屏幕。

附 實現大致方式代碼,有待優化

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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
 
package com.example.wanjian.test;
 
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.os.Environment;
import android.os.SystemClock;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.LinearLayout;
import android.widget.Toast;
 
import java.io.File;
import java.io.FileOutputStream;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
 
/**
* Created by wanjian on 16/8/18.
*/
public class ScrollableViewRECUtil {
 
public static final int VERTICAL = 0;
 
 
private static final int DELAY = 2;
 
private List<Bitmap> bitmaps = new ArrayList<>();
 
private int orientation = VERTICAL;
 
private View view;
 
private boolean isEnd;
 
private OnRecFinishedListener listener;
 
public ScrollableViewRECUtil(View view, int orientation) {
this.view = view;
this.orientation = orientation;
}
 
public void start(final OnRecFinishedListener listener) {
this.listener = listener;
 
 
final MotionEvent motionEvent = MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, view.getWidth() / 2, view.getHeight() / 2, 0);
view.dispatchTouchEvent(motionEvent);
motionEvent.setAction(MotionEvent.ACTION_MOVE);
//滑動距離大於ViewConfiguration.get(view.getContext()).getScaledTouchSlop()時listview才開始滾動
motionEvent.setLocation(motionEvent.getX(), motionEvent.getY() - (ViewConfiguration.get(view.getContext()).getScaledTouchSlop() + 1));
view.dispatchTouchEvent(motionEvent);
 
motionEvent.setLocation(motionEvent.getX(), view.getHeight() / 2);
 
view.postDelayed( new Runnable() {
@Override
public void run() {
 
if (isEnd) {
 
//停止時正好一屏則全部繪制,否則繪制部分
if ((view.getHeight() / 2 - (int) motionEvent.getY()) % view.getHeight() == 0) {
Bitmap bitmap = rec();
bitmaps.add(bitmap);
} else {
 
Bitmap origBitmap = rec();
 
int y = view.getHeight() / 2 - (int) motionEvent.getY();
Bitmap bitmap = Bitmap.createBitmap(origBitmap, 0, view.getHeight() - y % view.getHeight(), view.getWidth(), y % view.getHeight());
bitmaps.add(bitmap);
 
origBitmap.recycle();
}
 
 
//最后一張可能高度不足view的高度
int h = view.getHeight() * (bitmaps.size() - 1);
Bitmap bitmap = bitmaps.get(bitmaps.size() - 1);
 
h = h + bitmap.getHeight();
 
Bitmap result = Bitmap.createBitmap(view.getWidth(), h, Bitmap.Config.RGB_565);
 
Canvas canvas = new Canvas();
canvas.setBitmap(result);
 
for (int i = 0; i < bitmaps.size(); i++) {
Bitmap b = bitmaps.get(i);
canvas.drawBitmap(b, 0, i * view.getHeight(), null);
b.recycle();
 
}
 
listener.onRecFinish(result);
 
 
return;
}
 
if ((view.getHeight() / 2 - (int) motionEvent.getY()) % view.getHeight() == 0) {
Bitmap bitmap = rec();
bitmaps.add(bitmap);
}
 
 
motionEvent.setAction(MotionEvent.ACTION_MOVE);
//模擬每次向上滑動一個像素,這樣可能導致滾動特別慢,實際使用時可以修改該值,但判斷是否正好滾動了
//一屏幕就不能簡單的根據 (view.getHeight() / 2 - (int) motionEvent.getY()) % view.getHeight() == 0 來確定了。
//可以每次滾動n個像素,當發現下次再滾動n像素時就超出一屏幕時可以改變n的值,保證下次滾動后正好是一屏幕,
//這樣就可以根據(view.getHeight() / 2 - (int) motionEvent.getY()) % view.getHeight() == 0來判斷要不要截屏了。
motionEvent.setLocation(( int) motionEvent.getX(), (int) motionEvent.getY() - 1);
 
view.dispatchTouchEvent(motionEvent);
 
view.postDelayed( this, DELAY);
 
}
}, DELAY);
}
 
public void stop() {
isEnd = true;
}
 
private Bitmap rec() {
Bitmap film = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.RGB_565);
Canvas canvas = new Canvas();
 
canvas.setBitmap(film);
 
view.draw(canvas);
 
 
return film;
 
}
 
public interface OnRecFinishedListener {
void onRecFinish(Bitmap bitmap);
}
 
 
}
 
```
 
 
activity代碼
 
``` java
 
setContentView(R.layout.activity_main4);
//
listview= (ListView) findViewById(R.id.listview);
 
listview.setAdapter( new BaseAdapter() {
@Override
public int getCount() {
return 100;
}
 
@Override
public Object getItem(int position) {
return null;
}
 
@Override
public long getItemId(int position) {
return 0;
}
 
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView==null){
Button button= (Button) LayoutInflater.from(getApplication()).inflate(R.layout.item,listview, false);
button.setText( ""+position);
return button;
}
 
((Button)convertView).setText( ""+position);
 
return convertView;
}
});
//
 
File file= new File(Environment.getExternalStorageDirectory(),"aaa");
file.mkdirs();
 
for (File f:file.listFiles()){
f.delete();
}
 
 
listview.getViewTreeObserver().addOnGlobalLayoutListener( new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
listview.getViewTreeObserver().removeGlobalOnLayoutListener( this);
start();
}
});
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
 
private void start(){
final View view=findViewById(R.id.view);
 
final ScrollableViewRECUtil scrollableViewRECUtil=new ScrollableViewRECUtil(view,ScrollableViewRECUtil.VERTICAL);
 
scrollableViewRECUtil.start( new ScrollableViewRECUtil.OnRecFinishedListener() {
@Override
public void onRecFinish(Bitmap bitmap) {
File f= Environment.getExternalStorageDirectory();
System.out.print(f.getAbsoluteFile().toString());
Toast.makeText(getApplicationContext(),f.getAbsolutePath(),Toast.LENGTH_LONG).show();
try {
bitmap.compress(Bitmap.CompressFormat.JPEG, 60,new FileOutputStream(new File(f,"rec"+System.currentTimeMillis()+".jpg")));
 
Toast.makeText(getApplicationContext(), "Success",Toast.LENGTH_LONG).show();
} catch (Exception e){
e.printStackTrace();
}
}
});
 
// scrollableViewRECUtil
 
view.postDelayed( new Runnable() {
@Override
public void run() {
scrollableViewRECUtil.stop();
}
}, 90*1000);
}

布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 
<?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:id="@+id/view"
android:orientation="vertical"
>
 
<ListView
android:id="@+id/listview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="#e1e1e1"
android:dividerHeight="2dp"
> </ListView>
</LinearLayout>

效果圖

  • 屏幕
    屏幕屏幕
  • 最終截屏

截屏截屏

可以看到毫無拼接痕跡。

來自我的博客

http://blog.csdn.net/qingchunweiliang/article/details/52248643


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM