Android开发弹出键盘布局闪动如何解决
弹出键盘布局闪动原理和解决
在开发中,遇到一个问题:做一个微信一样,表情输入和软键盘在切换的时候,聊天界面不闪动的问题。为了解决这个问题,需要知道一下Android的软键盘弹出的时候发生的几个变化。
当AndroidMainfest.xml 中配置android:windowSoftInputMode="adjustResize|stateHidden" 属性后,如果弹出软键盘,那么会重绘界面。基本流程如下(API 10):
1. Android 收到打开软键盘命令
2. Android 打开软键盘后,调用App 注册在AWM 中的接口,告知它,界面需要进行变化.
2.1 调用ViewRoot.java#W#resized(w,h)
2.2 调用viewRoot.dispatchResized()
2.3 构造Message msg = obtainMessage(reportDraw ? RESIZED_REPORT :RESIZED),然后post过去
3. 在RootView的handleMessage的case RESIZED_REPORT: 收到具体的大小,配置App Window的大小,特别是 bottom 的大小, 最后调用requestLayout进行重绘
在上述的过程中,我们可以发现,其实弹出键盘之后的界面闪动的核心是在于app 的window botton 属性进行变化了,从而导致整个ViewTree的高度变化。那么我们只要表情PANEL在父布局onMeasure之前,进行VISIBLE或者GONE处理,使得最终的所有子View的高度满足window height,既可以实现不闪动聊天页面。
其核心思想为:在父布局onMeasure之前,隐藏/显示 合适高度的VIEW,既可以使得其他子VIEW高度不变化,从而避免界面闪动。引用自http://blog.dreamtobe.cn/2015/09/01/keyboard-panel-switch/。
代码如下:
Layout:
<com.test.MyActivity.MyLineLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:id="@+id/mll_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:background="#f3f3f3"
android:id="@+id/fl_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<TextView
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="DARKGEM"/>
<LinearLayout
android:id="@+id/ll_edit"
android:layout_width="match_parent"
android:orientation="horizontal"
android:layout_height="50dp">
<EditText
android:id="@+id/et_input"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"/>
<Button
android:id="@+id/btn_trigger"
android:text="trigger"
android:layout_width="100dp"
android:layout_height="match_parent"/>
<FrameLayout
android:id="@+id/fl_panel"
android:background="#CCCCCC"
android:layout_width="match_parent"
android:layout_height="0dp"/>
Activity:
package com.test;
import android.app.Activity;
import android.content.Context;
import android.graphics.Rect;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
/**
*
* 聊天界面布局闪动处理, 基本原理如下: * 1. 弹出键盘的时候,会导致 RootView 的bottom 变小,直到容纳 键盘+虚拟按键 * 2. 收回键盘的时候,会导致 RootView的bottom 变大,直到容纳 虚拟键盘 * 3. 因为RootView bottom的变化,会导致整个布局高度(bottom - top)的变化,所以就会发生布局闪动的情况. 而为了 * 避免这种情况,只需要在发生变动的父布局调用 onMeasure() 之前,将子View的高度和配置为最终高度,既可以实现弹 * 出/收回键盘 不闪动的效果(如微信聊天界面)。 *
*/
public class MyActivity extends Activity {
MyLineLayout mll_main;
FrameLayout fl_list;
LinearLayout ll_edit;
EditText et_input;
Button btn_trigger;
FrameLayout fl_panel;
Rect rect = new Rect();
enum State {
//空状态
NONE,
//打开输入法状态
KEYBOARD,
//打开面板状态
PANEL,
}
State state = State.NONE;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mll_main = (MyLineLayout) findViewById(R.id.mll_main);
fl_list = (FrameLayout) findViewById(R.id.fl_list);
ll_edit = (LinearLayout) findViewById(R.id.ll_edit);
et_input = (EditText) findViewById(R.id.et_input);
btn_trigger = (Button) findViewById(R.id.btn_trigger);
fl_panel = (FrameLayout) findViewById(R.id.fl_panel);
mll_main.onMeasureListener = new MyLineLayout.OnMeasureListener() {
/**
* 可能会发生多次 调用的情况,因为存在 layout_weight 属性,需要2次测试,给定最终大小
* */
@Override
public void onMeasure(int maxHeight, int oldHeight, int nowHeight) {
switch (state) {
case NONE: {
}
break;
case PANEL: {
//state 处于 panel 状态只有一种可能,就是主动点击切换到panel,
//1.如果之前是keyboard状态,则在本次onMeasure的时候,一定要把panel显示出来
//避免 mll 刷动
//2. 如果之前处于 none状态,那么本次触发来自于 postDelay,可以忽略
fl_panel.setVisibility(View.VISIBLE);
}
break;
case KEYBOARD: {
//state = KEYBOARD 状态,只有一种可能,就是主动点击了 EditText
//1. 如果之前是panel状态,则一般已经有了固有高度,这个高度刚刚好满足键盘的高度,那么只用隐藏掉
//panel 既可以实现页面不进行刷新
//2. 如果之前为none状态,则可以忽略
fl_panel.setVisibility(View.GONE);
//处于键盘状态,需要更新键盘高度为面板的高度
if (oldHeight >= nowHeight) {
//记录当前的缩放大小为键盘大小
int h = maxHeight - nowHeight;
//避免 输入法 悬浮状态, 保留一个最低高度
if (h < 500) {
h = 500;
}
fl_panel.getLayoutParams().height = h;
}
}
break;
}
Log.d("SC_SIZE", String.format("onMeasure %d %d %d", maxHeight, nowHeight, oldHeight));
}
};
fl_list.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
hideSoftInputView();
fl_panel.setVisibility(View.GONE);
state = State.NONE;
return false;
}
});
et_input.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
state = State.KEYBOARD;
}
});
btn_trigger.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
switch (state) {
case NONE:
case KEYBOARD: {
hideSoftInputView();
state = State.PANEL;
//无论App 处于什么状态,都追加一个 显示 panel 的方法,避免处于非正常状态无法打开panel
getWindow().getDecorView().postDelayed(new Runnable() {
@Override
public void run() {
fl_panel.setVisibility(View.VISIBLE);
}
}, 100);
}
break;
case PANEL: {
state = State.NONE;
fl_panel.setVisibility(View.GONE);
}
break;
}
}
});
//设置基本panel 高度,以使得第一次能正常打开panel
getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
fl_panel.getLayoutParams().height = rect.height() / 2;
fl_panel.setVisibility(View.GONE);
}
/**
* 隐藏软键盘输入
*/
public void hideSoftInputView() {
InputMethodManager manager = ((InputMethodManager) this.getSystemService(Activity.INPUT_METHOD_SERVICE));
if (getWindow().getAttributes().softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) {
if (getCurrentFocus() != null && manager != null)
manager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
}
}
/**
* Created by Administrator on 2015/11/20.
*/
public static class MyLineLayout extends LinearLayout {
OnMeasureListener onMeasureListener;
int maxHeight = 0;
int oldHeight;
public MyLineLayout(Context context) {
super(context);
}
public MyLineLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int height = MeasureSpec.getSize(heightMeasureSpec);
if (onMeasureListener != null) {
onMeasureListener.onMeasure(maxHeight, oldHeight, height);
}
oldHeight = height;
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//之所以,在这里记录 maxHeight的大小,是因为 onMeasure 中可能多次调用,中间可能会逐步出现 ActionBar,BottomVirtualKeyboard,
//所以 onMeasure中获取的maxHeight存在误差
if (h > maxHeight) {
maxHeight = h;
}
Log.d("SC_SIZE", String.format("Size Change %d %d", h, oldh));
}
interface OnMeasureListener {
void onMeasure(int maxHeight, int oldHeight, int nowHeight);
}
}
}
测试效果图:
解决Andriod软键盘出现把原来的布局给顶上去的方法
决方法,在mainfest.xml中,对那个Activity加:
就不会把原来Activity的布局给顶上去了。
今天要做一个搜索功能,搜索界面采用AutoCompleteTextView做搜索条,然后下面用listview来显示搜索结果,而我的主界面是在底部用tab做了一个主界面导航,其中有一个搜索按钮,因为在搜索条中输入文字的时候会弹出软件盘,但是如果不做什么设置的话,软键盘弹出来的同时,会把我下面的tab导航给相应拉到屏幕的上面,界面显示的扭曲啊,后来找到一种解决方法,在相应的activity中(比如我这是tab的activity,用的是adjustpan)添加
android:windowSoftInputMode这个属性,下面详细说下这个属性:
& X! Q6c9 }% i. ]6 @0 Y" N6^ d {"X
windowSoftInputMode属性设置值说明。
attributes:
android:windowSoftInputMode
活动的主窗口如何与包含屏幕上的软键盘窗口交互。这个属性的设置将会影响两件事情7S7 U+ S! p7 s( U) n: t: m& N
:
1>
软键盘的状态——是否它是隐藏或显示——当活动5w$ r- U9 i" h. O' M" M
(Activity)成为用户关注的焦点。
2>
活动的主窗口调整——是否减少活动主窗口大小以便腾出空间放软键盘或是否当活动窗口的部分被软键盘覆盖时它的内容的当前焦点是可见的。
它的设置必须是下面列表中的一个值,或一个
”state…”值加一个+ s. Z" m5 u: {; k; B7v4 Q
”adjust…”值的组合。在任一组设置多个值——多个
”state…”values,例如&
mdash有未定义的结果。各个值之间用+H8 v$ Q# ~5 f3 B& `- G8 c$ y
|分开。例如
:
在这设置的值8A: N! L' x0 `: C
(除'H0 N" g, w2 W) K F# y2 l!c
"stateUnspecified"和
"adjustUnspecified"以外3 ^, p2E G: I2 y/ V
)将覆盖在主题中设置的值
将覆盖在主题中设置的值
值 | 描述 |
"stateUnspecified" | 软键盘的状态 |
"stateUnchanged" | 软键盘被保持无论它上次是什么状态,是否可见或隐藏,当主窗口出现在前面时。 |
"stateHidden" | 当用户选择该 |
"stateAlwaysHidden" | 软键盘总是被隐藏的,当该 |
"stateVisible" | 软键盘是可见的,当那个是正常合适的时& d% G.y8 [; G; _: v |
"stateAlwaysVisible" | 当用户选择这个6 Z%C |
"adjustUnspecified" | 它不被指定是否该" H9 b! V3 h5_& O$ d$ M |
"adjustResize" | 该/ M" R: m- W( Z. Q6 d*A |
"adjustPan" | 该 |
1.使用多线程实现文件下载...
多线程下载是加快下载速度的一种方式..通过开启多个线程去执行一个任务..可以使任务的执行速度变快..多线程的任务下载时常都会使用得到..比如说我们手机内部应用宝的下载机制..一定是通过使用了多线程创建的下载器..并且这个下载器可以实现断点下载..在任务被强行终止之后..下次可以通过触发按钮来完成断点下载...那么如何实现断点下载这就是一个问题了..
首先我们需要明确一点就是多线程下载器通过使用多个线程对同一个任务进行下载..但是这个多线程并不是线程的数目越多,下载的速度就越快..当线程增加的很多的时候,单个线程执行效率也会变慢..因此线程的数目需要有一个限度..经过楼主亲自测试..多线程下载同一个任务时,线程的数目5-8个算是比较高效的..当线程的数量超过10个之后,那么多线程的效率反而就变慢了(前提:网速大体相同的时候..)
那么在实现多线程下载同一个任务的时候我们需要明白其中的道理..下面先看一张附加图..
这个图其实就很简单的说明了其中的原理..我们将一个任务分成多个部分..然后开启多个线程去下载对应的部分就可以了..那么首先要解决的问题就是如何使各自的线程去下载各自对应的任务,不能越界..那么我们来看看具体的实现过程..首先我们使用普通Java代码去实现..最后将Java代码移植到Android就可以了..
public class Download { public static int threadCount = 5; //线程开启的数量.. public static void main(String[] args) { // TODO Auto-generated method stub String path = "http://192.168.199.172:8080/jdk.exe"; try { URL url = new URL(path); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(5000); int status = conn.getResponseCode(); if(status == 200){ int length = conn.getContentLength(); System.out.println(length); int blocksize = length/threadCount; //将文件长度进行平分.. for(int threadID=1; threadID<=threadCount;threadID++){ int startIndex = (threadID-1)*blocksize; //开始位置的求法.. int endIndex = threadID*blocksize -1; //结束位置的求法.. /** * 如果一个文件的长度无法整除线程数.. * 那么最后一个线程下载的结束位置需要设置文件末尾.. * */ if(threadID == threadCount){ endIndex = length; } System.out.println("线程下载位置:"+startIndex+"---"+endIndex); } } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
这样只是实现了通过连接服务器来获取文件的长度..然后去设置每一个线程下载的开始位置和结束位置..这里只是完成了这些步骤..有了下载的开始位置和结束位置..我们就需要开启线程来完成下载了...因此我们需要自己定义下载的过程...
首先我们需要明确思路:既然是断点下载..那么如果一旦发生了断点情况..我们在下一次进行下载的时候需要从原来断掉的位置进行下载..已经下载过的位置我们就不需要进行下载了..因此我们需要记载每一次的下载记录..那么有了记录之后..一旦文件下载完成之后..这些记录就需要被清除掉...因此明确了这两个地方的思路就很容易书写了..
//下载线程的定义.. public static class DownLoadThread implements Runnable{ private int threadID; private int startIndex; private int endIndex; private String path; public DownLoadThread(int threadID,int startIndex,int endIndex,String path){ this.threadID = threadID; this.startIndex = startIndex; this.endIndex = endIndex; this.path = path; } @Override public void run() { // TODO Auto-generated method stub try { //判断上一次是否下载完毕..如果没有下载完毕需要继续进行下载..这个文件记录了上一次的下载位置.. File tempfile =new File(threadID+".txt"); if(tempfile.exists() && tempfile.length()>0){ FileInputStream fis = new FileInputStream(tempfile); byte buffer[] = new byte[1024]; int leng = fis.read(buffer); int downlength = Integer.parseInt(new String(buffer,0,leng));//从上次下载后的位置开始下载..重新拟定开始下载的位置.. startIndex = downlength; fis.close(); } URL url = new URL(path); HttpURLConnection conn =(HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(5000); conn.setRequestProperty("Range", "bytes="+startIndex+"-"+endIndex); int status = conn.getResponseCode(); //206也表示服务器响应成功.. if(status == 206){ //获取服务器返回的I/O流..然后将数据写入文件当中.. InputStream in = conn.getInputStream(); //文件写入开始..用来保存当前需要下载的文件.. RandomAccessFile raf = new RandomAccessFile("jdk.exe", "rwd"); raf.seek(startIndex); int len = 0; byte buf[] =new byte[1024]; //记录已经下载的长度.. int total = 0; while((len = in.read(buf))!=-1){ //用于记录当前下载的信息.. RandomAccessFile file =new RandomAccessFile(threadID+".txt", "rwd"); total += len; file.write((total+startIndex+"").getBytes()); file.close(); //将数据写入文件当中.. raf.write(buf, 0, len); } in.close(); raf.close(); } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); }finally{ //如果所有的线程全部下载完毕后..也就是任务完成..清除掉所有原来的记录文件.. runningThread -- ; if(runningThread==0){ for(int i=1;i<threadCount;i++){ File file = new File(i+".txt"); file.delete(); } } } } }
这样就完成了文件的数据信息的下载..经过测试..一个13M的文件在5个线程共同作用下下载的时间差不多是12秒左右(网速稳定在300k的情况下..带宽越宽..速度就会更快)单个线程下载的时间差不多是15秒左右..这里才缩短了两秒钟的时间..但是我们不要忘记..如果文件过大的话呢?因此楼主亲测了一下一个90M的文件在5个线程同时作用下时间差不多1分20秒左右..而使用一个线程进行下载差不多2分钟左右..这里还是缩短了大量的时间..
因此根据对比,还是使用多个线程来进行下载更加的好一些..虽然Android里的一般应用不会超过50M左右..但是游戏的话一般差不多能达到100-200M左右..因此使用多线程还是能够提高下载的进度和效率..同样我们可以通过使用线程池的方式去开启线程..最后这些线程交给线程池去管理就可以了..
在正常的Java项目中我们书写好了下载代码..就可以移植到我们的Android应用程序当中..但是还是有一些地方需要注意..因此在这里去强调一下..
package com.example.mutithread; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.URL; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.app.Activity; import android.text.TextUtils; import android.view.Menu; import android.view.View; import android.widget.EditText; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; public class MainActivity extends Activity { private EditText et; private ProgressBar pb; public static int threadCount = 5; public static int runningThread=5; public int currentProgress=0; //当前进度值.. private TextView tv; private Handler handler = new Handler(){ @Override public void handleMessage(Message msg){ switch (msg.what) { case 1: Toast.makeText(getApplicationContext(), msg.obj.toString(), 0).show(); break; case 2: break; case 3: tv.setText("当前进度:"+(pb.getProgress()*100)/pb.getMax()); default: break; } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); et = (EditText) findViewById(R.id.et); pb =(ProgressBar) findViewById(R.id.pg); tv= (TextView) findViewById(R.id.tv); } public void downLoad(View v){ final String path = et.getText().toString().trim(); if(TextUtils.isEmpty(path)){ Toast.makeText(this, "下载路径错误", Toast.LENGTH_LONG).show(); return ; } new Thread(){ // String path = "http://192.168.199.172:8080/jdk.exe"; public void run(){ try { URL url = new URL(path); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(5000); int status = conn.getResponseCode(); if(status == 200){ int length = conn.getContentLength(); System.out.println("文件总长度"+length); pb.setMax(length); RandomAccessFile raf = new RandomAccessFile("/sdcard/setup.exe","rwd"); raf.setLength(length); raf.close(); //开启5个线程来下载当前资源.. int blockSize = length/threadCount ; for(int threadID=1;threadID<=threadCount;threadID++){ int startIndex = (threadID-1)*blockSize; int endIndex = threadID*blockSize -1; if(threadID == threadCount){ endIndex = length; } System.out.println("线程"+threadID+"下载:---"+startIndex+"--->"+endIndex); new Thread(new DownLoadThread(threadID, startIndex, endIndex, path)).start() ; } } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } }; }.start(); } /** * 下载线程.. * */ public class DownLoadThread implements Runnable{ private int ThreadID; private int startIndex; private int endIndex; private String path; public DownLoadThread(int ThreadID,int startIndex,int endIndex,String path){ this.ThreadID = ThreadID; this.startIndex = startIndex; this.endIndex = endIndex; this.path = path; } @Override public void run() { // TODO Auto-generated method stub URL url; try { //检查是否存在还未下载完成的文件... File tempfile = new File("/sdcard/"+ThreadID+".txt"); if(tempfile.exists() && tempfile.length()>0){ FileInputStream fis = new FileInputStream(tempfile); byte temp[] =new byte[1024]; int leng = fis.read(temp); int downlength = Integer.parseInt(new String(temp,0,leng)); int alreadydown = downlength -startIndex; currentProgress += alreadydown;//发生断点之后记录下载的文件长度.. startIndex = downlength; fis.close(); } url = new URL(path); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("Range", "bytes="+startIndex+"-"+endIndex); conn.setConnectTimeout(5000); //获取响应码.. int status =conn.getResponseCode(); if(status == 206){ InputStream in = conn.getInputStream(); RandomAccessFile raf =new RandomAccessFile("/sdcard/jdk.exe", "rwd"); //文件开始写入.. raf.seek(startIndex); int len =0; byte[] buffer =new byte[1024]; //已经下载的数据长度.. int total = 0; while((len = in.read(buffer))!=-1){ //记录当前数据下载的长度... RandomAccessFile file = new RandomAccessFile("/sdcard/"+ThreadID+".txt", "rwd"); raf.write(buffer, 0, len); total += len; System.out.println("线程"+ThreadID+"total:"+total); file.write((total+startIndex+"").getBytes()); file.close(); synchronized (MainActivity.this) { currentProgress += len; //获取当前总进度... //progressBar progressDialog可以直接在子线程内部更新UI..由于源码内部进行了特殊的处理.. pb.setProgress(currentProgress); //更改界面上的进度条进度.. Message msg =Message.obtain(); //复用以前的消息..避免多次new... msg.what = 3; handler.sendMessage(msg); } } raf.close(); in.close(); System.out.println("线程:"+ThreadID+"下载完毕"); }else{ System.out.println("线程:"+ThreadID+"下载失败"); } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); Message msg = new Message(); msg.what = 1; msg.obj = e; handler.sendMessage(msg); }finally{ synchronized (MainActivity.this) { runningThread--; if(runningThread == 0){ for(int i=1;i<=threadCount;i++){ File file = new File("/sdcard/"+i+".txt"); file.delete(); } Message msg =new Message(); msg.what = 2; msg.obj ="下载完毕"; handler.sendMessage(msg); } } } } } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } }
源代码如上..优化的事情我就不做了..为了方便直接就贴上了..这里定义了一个ProgressBar进度条..一个TextView来同步进度条的下载进度..在Android中我们自然不能够在主线程中去调用耗时间的操作..因此这些耗时的操作我们就通过开启子线程的方式去使用..但是子线程是不能够更新UI界面的..因此我们需要使用到Handler Message机制来完成主界面UI更新的操作.
但是上面的代码当中我们会发现一个问题..在子线程内部居然更新了ProgressBar操作..其实ProgressBar和ProgressDialog是两个特例..我们可以在子线程内部去更新他们的属性..我们来看一下源代码的实现过程..
private synchronized void refreshProgress(int id, int progress, boolean fromUser) { if (mUiThreadId == Thread.currentThread().getId()) { //如果当前运行的线程和主线程相同..那么更新进度条.. doRefreshProgress(id, progress, fromUser, true); } else { //如果不满足上面说的情况.. if (mRefreshProgressRunnable == null) { mRefreshProgressRunnable = new RefreshProgressRunnable();//那么新建立一个线程..然后执行下面的过程.. } final RefreshData rd = RefreshData.obtain(id, progress, fromUser); //获取消息队列中的消息.. mRefreshData.add(rd); if (mAttached && !mRefreshIsPosted) { post(mRefreshProgressRunnable); //主要是这个地方..调用了post方法..将当前运行的线程发送到消息队列当中..那么这个线程就可以在UI中运行了..因此这一步是决定因素.. mRefreshIsPosted = true; } } }
正是由于源码内部调用了post方法..将当前的线程放入到消息队列当中..那么UI中的Looper线程就会对这个线程进行处理..那么就表示这个线程是可以被执行在UI当中的..也正是这个因素导致了我们可以在子线程内部更新ProgressBar..但是我们可以看到如果我们想要去更新TextView的时候..我们就需要调用Handler Message机制来完成UI界面的更新了..因此这一块需要我们去注意..
移植之后代码其实并没有发生太大的变化..这样就可以完成一个在Android中的多线程断点下载器了..
Android开发多线程断点续传下载器
使用多线程断点续传下载器在下载的时候多个线程并发可以占用服务器端更多资源,从而加快下载速度,在下载过程中记录每个线程已拷贝数据的数量,如果下载中断,比如无信号断线、电量不足等情况下,这就需要使用到断点续传功能,下次启动时从记录位置继续下载,可避免重复部分的下载。这里采用数据库来记录下载的进度。
效果图
断点续传
1.断点续传需要在下载过程中记录每条线程的下载进度
2.每次下载开始之前先读取数据库,查询是否有未完成的记录,有就继续下载,没有则创建新记录插入数据库
3.在每次向文件中写入数据之后,在数据库中更新下载进度
4.下载完成之后删除数据库中下载记录
Handler传输数据
这个主要用来记录百分比,每下载一部分数据就通知主线程来记录时间
1.主线程中创建的View只能在主线程中修改,其他线程只能通过和主线程通信,在主线程中改变View数据
2.我们使用Handler可以处理这种需求
主线程中创建Handler,重写handleMessage()方法
新线程中使用Handler发送消息,主线程即可收到消息,并且执行handleMessage()方法
动态生成新View
可实现多任务下载
1.创建XML文件,将要生成的View配置好
2.获取系统服务LayoutInflater,用来生成新的View
LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
3.使用inflate(int resource, ViewGroup root)方法生成新的View
4.调用当前页面中某个容器的addView,将新创建的View添加进来
示例
进度条样式 download.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" > <LinearLayout android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_weight="1" > <!--进度条样式默认为圆形进度条,水平进度条需要配置style属性, ?android:attr/progressBarStyleHorizontal --> <ProgressBar android:layout_width="fill_parent" android:layout_height="20dp" style="?android:attr/progressBarStyleHorizontal" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="0%" /> </LinearLayout> <Button android:layout_width="40dp" android:layout_height="40dp" android:onClick="pause" android:text="||" /> </LinearLayout>
顶部样式 main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:id="@+id/root" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="请输入下载路径" /> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginBottom="30dp" > <EditText android:id="@+id/path" android:layout_width="fill_parent" android:layout_height="wrap_content" android:singleLine="true" android:layout_weight="1" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="下载" android:onClick="download" /> </LinearLayout> </LinearLayout>
MainActivity.java
public class MainActivity extends Activity { private LayoutInflater inflater; private LinearLayout rootLinearLayout; private EditText pathEditText; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); //动态生成新View,获取系统服务LayoutInflater,用来生成新的View inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); rootLinearLayout = (LinearLayout) findViewById(R.id.root); pathEditText = (EditText) findViewById(R.id.path); // 窗体创建之后, 查询数据库是否有未完成任务, 如果有, 创建进度条等组件, 继续下载 List<String> list = new InfoDao(this).queryUndone(); for (String path : list) createDownload(path); } /** * 下载按钮 * @param view */ public void download(View view) { String path = "http://192.168.1.199:8080/14_Web/" + pathEditText.getText().toString(); createDownload(path); } /** * 动态生成新View * 初始化表单数据 * @param path */ private void createDownload(String path) { //获取系统服务LayoutInflater,用来生成新的View LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); LinearLayout linearLayout = (LinearLayout) inflater.inflate(R.layout.download, null); LinearLayout childLinearLayout = (LinearLayout) linearLayout.getChildAt(0); ProgressBar progressBar = (ProgressBar) childLinearLayout.getChildAt(0); TextView textView = (TextView) childLinearLayout.getChildAt(1); Button button = (Button) linearLayout.getChildAt(1); try { button.setOnClickListener(new MyListener(progressBar, textView, path)); //调用当前页面中某个容器的addView,将新创建的View添加进来 rootLinearLayout.addView(linearLayout); } catch (Exception e) { e.printStackTrace(); } } private final class MyListener implements OnClickListener { private ProgressBar progressBar; private TextView textView; private int fileLen; private Downloader downloader; private String name; /** * 执行下载 * @param progressBar //进度条 * @param textView //百分比 * @param path //下载文件路径 */ public MyListener(ProgressBar progressBar, TextView textView, String path) { this.progressBar = progressBar; this.textView = textView; name = path.substring(path.lastIndexOf("/") + 1); downloader = new Downloader(getApplicationContext(), handler); try { downloader.download(path, 3); } catch (Exception e) { e.printStackTrace(); Toast.makeText(getApplicationContext(), "下载过程中出现异常", 0).show(); throw new RuntimeException(e); } } //Handler传输数据 private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case 0: //获取文件的大小 fileLen = msg.getData().getInt("fileLen"); //设置进度条最大刻度:setMax() progressBar.setMax(fileLen); break; case 1: //获取当前下载的总量 int done = msg.getData().getInt("done"); //当前进度的百分比 textView.setText(name + "\t" + done * 100 / fileLen + "%"); //进度条设置当前进度:setProgress() progressBar.setProgress(done); if (done == fileLen) { Toast.makeText(getApplicationContext(), name + " 下载完成", 0).show(); //下载完成后退出进度条 rootLinearLayout.removeView((View) progressBar.getParent().getParent()); } break; } } }; /** * 暂停和继续下载 */ public void onClick(View v) { Button pauseButton = (Button) v; if ("||".equals(pauseButton.getText())) { downloader.pause(); pauseButton.setText("▶"); } else { downloader.resume(); pauseButton.setText("||"); } } } }
Downloader.java
public class Downloader { private int done; private InfoDao dao; private int fileLen; private Handler handler; private boolean isPause; public Downloader(Context context, Handler handler) { dao = new InfoDao(context); this.handler = handler; } /** * 多线程下载 * @param path 下载路径 * @param thCount 需要开启多少个线程 * @throws Exception */ public void download(String path, int thCount) throws Exception { URL url = new URL(path); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); //设置超时时间 conn.setConnectTimeout(3000); if (conn.getResponseCode() == 200) { fileLen = conn.getContentLength(); String name = path.substring(path.lastIndexOf("/") + 1); File file = new File(Environment.getExternalStorageDirectory(), name); RandomAccessFile raf = new RandomAccessFile(file, "rws"); raf.setLength(fileLen); raf.close(); //Handler发送消息,主线程接收消息,获取数据的长度 Message msg = new Message(); msg.what = 0; msg.getData().putInt("fileLen", fileLen); handler.sendMessage(msg); //计算每个线程下载的字节数 int partLen = (fileLen + thCount - 1) / thCount; for (int i = 0; i < thCount; i++) new DownloadThread(url, file, partLen, i).start(); } else { throw new IllegalArgumentException("404 path: " + path); } } private final class DownloadThread extends Thread { private URL url; private File file; private int partLen; private int id; public DownloadThread(URL url, File file, int partLen, int id) { this.url = url; this.file = file; this.partLen = partLen; this.id = id; } /** * 写入操作 */ public void run() { // 判断上次是否有未完成任务 Info info = dao.query(url.toString(), id); if (info != null) { // 如果有, 读取当前线程已下载量 done += info.getDone(); } else { // 如果没有, 则创建一个新记录存入 info = new Info(url.toString(), id, 0); dao.insert(info); } int start = id * partLen + info.getDone(); // 开始位置 += 已下载量 int end = (id + 1) * partLen - 1; try { HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setReadTimeout(3000); //获取指定位置的数据,Range范围如果超出服务器上数据范围, 会以服务器数据末尾为准 conn.setRequestProperty("Range", "bytes=" + start + "-" + end); RandomAccessFile raf = new RandomAccessFile(file, "rws"); raf.seek(start); //开始读写数据 InputStream in = conn.getInputStream(); byte[] buf = new byte[1024 * 10]; int len; while ((len = in.read(buf)) != -1) { if (isPause) { //使用线程锁锁定该线程 synchronized (dao) { try { dao.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } raf.write(buf, 0, len); done += len; info.setDone(info.getDone() + len); // 记录每个线程已下载的数据量 dao.update(info); //新线程中用Handler发送消息,主线程接收消息 Message msg = new Message(); msg.what = 1; msg.getData().putInt("done", done); handler.sendMessage(msg); } in.close(); raf.close(); // 删除下载记录 dao.deleteAll(info.getPath(), fileLen); } catch (IOException e) { e.printStackTrace(); } } } //暂停下载 public void pause() { isPause = true; } //继续下载 public void resume() { isPause = false; //恢复所有线程 synchronized (dao) { dao.notifyAll(); } } } Dao: DBOpenHelper: public class DBOpenHelper extends SQLiteOpenHelper { public DBOpenHelper(Context context) { super(context, "download.db", null, 1); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE info(path VARCHAR(1024), thid INTEGER, done INTEGER, PRIMARY KEY(path, thid))"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } }
InfoDao:
public class InfoDao { private DBOpenHelper helper; public InfoDao(Context context) { helper = new DBOpenHelper(context); } public void insert(Info info) { SQLiteDatabase db = helper.getWritableDatabase(); db.execSQL("INSERT INTO info(path, thid, done) VALUES(?, ?, ?)", new Object[] { info.getPath(), info.getThid(), info.getDone() }); } public void delete(String path, int thid) { SQLiteDatabase db = helper.getWritableDatabase(); db.execSQL("DELETE FROM info WHERE path=? AND thid=?", new Object[] { path, thid }); } public void update(Info info) { SQLiteDatabase db = helper.getWritableDatabase(); db.execSQL("UPDATE info SET done=? WHERE path=? AND thid=?", new Object[] { info.getDone(), info.getPath(), info.getThid() }); } public Info query(String path, int thid) { SQLiteDatabase db = helper.getWritableDatabase(); Cursor c = db.rawQuery("SELECT path, thid, done FROM info WHERE path=? AND thid=?", new String[] { path, String.valueOf(thid) }); Info info = null; if (c.moveToNext()) info = new Info(c.getString(0), c.getInt(1), c.getInt(2)); c.close(); return info; } public void deleteAll(String path, int len) { SQLiteDatabase db = helper.getWritableDatabase(); Cursor c = db.rawQuery("SELECT SUM(done) FROM info WHERE path=?", new String[] { path }); if (c.moveToNext()) { int result = c.getInt(0); if (result == len) db.execSQL("DELETE FROM info WHERE path=? ", new Object[] { path }); } } public List<String> queryUndone() { SQLiteDatabase db = helper.getWritableDatabase(); Cursor c = db.rawQuery("SELECT DISTINCT path FROM info", null); List<String> pathList = new ArrayList<String>(); while (c.moveToNext()) pathList.add(c.getString(0)); c.close(); return pathList; } }
先看看效果图
Activity:
package com.example.editortoast; import android.app.Activity; import android.os.Bundle; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.widget.TextView; import android.widget.Toast; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.bt).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { toastShow(); } }); } private void toastShow() { LayoutInflater inflater = LayoutInflater.from(getApplicationContext()); View view = inflater.inflate(R.layout.item_toast, null); TextView textView1 = (TextView) view.findViewById(R.id.TextView_1); textView1.setText("Toast1"); Toast toast = new Toast(getApplicationContext()); toast.setGravity(Gravity.CENTER_VERTICAL, 0, 0); toast.setDuration(0); toast.setView(view); toast.show(); } }
activity_main.xml:
<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="com.example.editortoast.MainActivity" > <Button android:id="@+id/bt" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="点击" />
</RelativeLayout>
item_toast.xml:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <ImageView android:id="@+id/image" android:layout_width="80dp" android:layout_height="80dp" android:layout_centerVertical="true" android:src="@drawable/ic_launcher" /> <TextView android:id="@+id/TextView_1" android:textSize="30sp" android:textColor="@android:color/holo_red_light" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_toRightOf="@id/image" /> </RelativeLayout>
android 自定义Toast,可设定显示时间
开发android的同学可能会抱怨Toast设定显示的时长无效,只能是Toast.LENGTH_LONG 或者Toast.LENGTH_SHORT 之一,为了解决这些办法,有多种实现方式:
1.使用定时器,定时调用show()方法.
2.使用CountDownTimer类,也是调用show()方法.
3.使用WindownManager类实现.
本文使用方法三进行实现,难度不大,直接看代码吧.
package com.open.toast; import android.content.Context; import android.graphics.Color; import android.graphics.PixelFormat; import android.os.Handler; import android.view.Gravity; import android.view.View; import android.view.WindowManager; import android.widget.LinearLayout; import android.widget.TextView; /** * 自定义时长的Toast * @author DexYang * */ public class CToast { public static CToast makeText(Context context, CharSequence text, int duration) { CToast result = new CToast(context); LinearLayout mLayout=new LinearLayout(context); TextView tv = new TextView(context); tv.setText(text); tv.setTextColor(Color.WHITE); tv.setGravity(Gravity.CENTER); mLayout.setBackgroundResource(R.drawable.widget_toast_bg); int w=context.getResources().getDisplayMetrics().widthPixels / 2; int h=context.getResources().getDisplayMetrics().widthPixels / 10; mLayout.addView(tv, w, h); result.mNextView = mLayout; result.mDuration = duration; return result; } public static final int LENGTH_SHORT = 2000; public static final int LENGTH_LONG = 3500; private final Handler mHandler = new Handler(); private int mDuration=LENGTH_SHORT; private int mGravity = Gravity.CENTER; private int mX, mY; private float mHorizontalMargin; private float mVerticalMargin; private View mView; private View mNextView; private WindowManager mWM; private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams(); public CToast(Context context) { init(context); } /** * Set the view to show. * @see #getView */ public void setView(View view) { mNextView = view; } /** * Return the view. * @see #setView */ public View getView() { return mNextView; } /** * Set how long to show the view for. * @see #LENGTH_SHORT * @see #LENGTH_LONG */ public void setDuration(int duration) { mDuration = duration; } /** * Return the duration. * @see #setDuration */ public int getDuration() { return mDuration; } /** * Set the margins of the view. * * @param horizontalMargin The horizontal margin, in percentage of the * container width, between the container's edges and the * notification * @param verticalMargin The vertical margin, in percentage of the * container height, between the container's edges and the * notification */ public void setMargin(float horizontalMargin, float verticalMargin) { mHorizontalMargin = horizontalMargin; mVerticalMargin = verticalMargin; } /** * Return the horizontal margin. */ public float getHorizontalMargin() { return mHorizontalMargin; } /** * Return the vertical margin. */ public float getVerticalMargin() { return mVerticalMargin; } /** * Set the location at which the notification should appear on the screen. * @see android.view.Gravity * @see #getGravity */ public void setGravity(int gravity, int xOffset, int yOffset) { mGravity = gravity; mX = xOffset; mY = yOffset; } /** * Get the location at which the notification should appear on the screen. * @see android.view.Gravity * @see #getGravity */ public int getGravity() { return mGravity; } /** * Return the X offset in pixels to apply to the gravity's location. */ public int getXOffset() { return mX; } /** * Return the Y offset in pixels to apply to the gravity's location. */ public int getYOffset() { return mY; } /** * schedule handleShow into the right thread */ public void show() { mHandler.post(mShow); if(mDuration>0) { mHandler.postDelayed(mHide, mDuration); } } /** * schedule handleHide into the right thread */ public void hide() { mHandler.post(mHide); } private final Runnable mShow = new Runnable() { public void run() { handleShow(); } }; private final Runnable mHide = new Runnable() { public void run() { handleHide(); } }; private void init(Context context) { final WindowManager.LayoutParams params = mParams; params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.width = WindowManager.LayoutParams.WRAP_CONTENT; params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON; params.format = PixelFormat.TRANSLUCENT; params.windowAnimations = android.R.style.Animation_Toast; params.type = WindowManager.LayoutParams.TYPE_TOAST; params.setTitle("Toast"); mWM = (WindowManager) context.getApplicationContext() .getSystemService(Context.WINDOW_SERVICE); } private void handleShow() { if (mView != mNextView) { // remove the old view if necessary handleHide(); mView = mNextView; // mWM = WindowManagerImpl.getDefault(); final int gravity = mGravity; mParams.gravity = gravity; if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) { mParams.horizontalWeight = 1.0f; } if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) { mParams.verticalWeight = 1.0f; } mParams.x = mX; mParams.y = mY; mParams.verticalMargin = mVerticalMargin; mParams.horizontalMargin = mHorizontalMargin; if (mView.getParent() != null) { mWM.removeView(mView); } mWM.addView(mView, mParams); } } private void handleHide() { if (mView != null) { if (mView.getParent() != null) { mWM.removeView(mView); } mView = null; } } }
测试类的代码如下:
package com.open.toast; import android.app.Activity; import android.os.Bundle; import android.text.TextUtils; import android.view.View; import android.widget.EditText; public class MainActivity extends Activity { private EditText mEditText; private CToast mCToast; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); init(); } private void init() { mEditText=(EditText)findViewById(R.id.timeEditText); findViewById(R.id.showToastBtn).setOnClickListener(listener); findViewById(R.id.hideToastBtn).setOnClickListener(listener); } private View.OnClickListener listener=new View.OnClickListener() { @Override public void onClick(View v) { switch(v.getId()) { case R.id.showToastBtn: if(null!=mCToast) { mCToast.hide(); } int time=TextUtils.isEmpty(mEditText.getText().toString())?CToast.LENGTH_SHORT:Integer.valueOf(mEditText.getText().toString()); mCToast=CToast.makeText(getApplicationContext(), "我来自CToast!",time); mCToast.show(); break; case R.id.hideToastBtn: if(null!=mCToast) { mCToast.hide(); } break; } } }; }
效果如下:
Android ScrollView滚动机制
我们都知道通过View#scrollTo(x,y)既可以实现将View滚动的效果,如果再添加Scroller类,就可以实现滚到效果。但是,这背后是如何实现的呢?这个问题涉及到View的绘图机制。我们先看看View的绘图的基本流程
(图片来自于网上比较常见的view绘图流程图)
关于三个阶段的简单描述:
1. measure:预估计ViewTree的各个View的占用空间。
2. layout : 确定ViewTree中各个View所处的空间位置,包括width,height,left,top,right,bottom
3. draw: 使用RootViewImpl中的一个surface.lockCanvas(dirty)对象作为画布,然ViewTree上所有的View都在这个Canvas上进行画图,
值得注意的是,Canvas通过getHeight() 和 getWidth()就是整个屏幕的真实大小。包括了通知栏(虽然在打印出来的ViewTree看不到,但是通过top属性,留下了一点空间给通知栏),标题栏,Content,底部虚拟按键等。
我们先看看mScrollX/mScrollY在代码中的注释:
mScrollX/mScrollY相对这个View的内容(文字,图片,子View)垂直/水平的像素偏移。如下图:
在设置mScrollX / mScrollY后,就可以滚动到指定的“内容",而mScrollX/mScrollY 就是相对于“内容”的偏移量,内容原点为(0,0)。
而这种内容大小以及偏移是如何发生的?在ViewGroup中,存在一个API drawChild(),这个函数主要完成对子View的空间大小的限制以及偏移,见如下的描述
protected boolean drawChild(Canvas canvas, View child, long drawingTime) { boolean more = false; //获取子View的空间大小 final int cl = child.mLeft; final int ct = child.mTop; final int cr = child.mRight; final int cb = child.mBottom; //通知子View进行判断是否完成滚动,这里就是通过Scroller代码实现滚动的关键点 child.computeScroll(); //获取最新的偏移量 final int sx = child.mScrollX; final int sy = child.mScrollY; //创建一个还原点 final int restoreTo = canvas.save(); //偏移,通过这个API,实现了scroll对内容偏移, 先把内容的原点进行偏移到负数区域 canvas.translate(cl - sx, ct - sy); //剪切,因为之前有一个translate操作,所有剪切出来的空间就是父View给定的可见区域 //所以如果子View填充Canvas的内容超出给定的空间,也不会显示出来 canvas.clipRect(sx, sy, sx + (cr - cl), sy + (cb - ct)); //让子View进行绘图,注意子View不用处理Scroll属性,既可以实现内容偏移 child.draw(canvas); //还原 canvas.restoreToCount(restoreTo); return more; }
值得注意的是,ListView不是采用这种机制实现的,而是采用替换ChildView来实现滑动效果的。
Android 嵌套滑动机制(NestedScrolling)
Android 在发布 Lollipop版本之后,为了更好的用户体验,Google为Android的滑动机制提供了NestedScrolling特性
NestedScrolling的特性可以体现在哪里呢?
比如你使用了Toolbar,下面一个ScrollView,向上滚动隐藏Toolbar,向下滚动显示Toolbar,这里在逻辑上就是一个NestedScrolling —— 因为你在滚动整个Toolbar在内的View的过程中,又嵌套滚动了里面的ScrollView。
效果如上图
在这之前,我们知道Android对Touch事件的分发是有自己一套机制的。主要是有是三个函数:
dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。
这种分发机制有一个漏洞:
如果子view获得处理touch事件机会的时候,父view就再也没有机会去处理这个touch事件了,直到下一次手指再按下。
也就是说,我们在滑动子View的时候,如果子View对这个滑动事件不想要处理的时候,只能抛弃这个touch事件,而不会把这些传给父view去处理。
但是Google新的NestedScrolling机制就很好的解决了这个问题。
我们看看如何实现这个NestedScrolling,首先有几个类(接口)我们需要关注一下
NestedScrollingChild NestedScrollingParent NestedScrollingChildHelper NestedScrollingParentHelper
以上四个类都在support-v4包中提供,Lollipop的View默认实现了几种方法。
实现接口很简单,这边我暂时用到了NestedScrollingChild系列的方法(因为Parent是support-design提供的CoordinatorLayout)
@Override public void setNestedScrollingEnabled(boolean enabled) { super.setNestedScrollingEnabled(enabled); mChildHelper.setNestedScrollingEnabled(enabled); } @Override public boolean isNestedScrollingEnabled() { return mChildHelper.isNestedScrollingEnabled(); } @Override public boolean startNestedScroll(int axes) { return mChildHelper.startNestedScroll(axes); } @Override public void stopNestedScroll() { mChildHelper.stopNestedScroll(); } @Override public boolean hasNestedScrollingParent() { return mChildHelper.hasNestedScrollingParent(); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); } @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); }
对,简单的话你就这么实现就好了。
这些接口都是我们在需要的时候自己调用的。childHelper干了些什么事呢?,看一下startNestedScroll方法
/** * Start a new nested scroll for this view. * * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass * method/{@link NestedScrollingChild} interface method with the same signature to implement * the standard policy.</p> * * @param axes Supported nested scroll axes. * See {@link NestedScrollingChild#startNestedScroll(int)}. * @return true if a cooperating parent view was found and nested scrolling started successfully */ public boolean startNestedScroll(int axes) { if (hasNestedScrollingParent()) { // Already in progress return true; } if (isNestedScrollingEnabled()) { ViewParent p = mView.getParent(); View child = mView; while (p != null) { if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) { mNestedScrollingParent = p; ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes); return true; } if (p instanceof View) { child = (View) p; } p = p.getParent(); } } return false; }
可以看到这里是帮你实现一些跟NestedScrollingParent交互的一些方法。
ViewParentCompat是一个和父view交互的兼容类,它会判断api version,如果在Lollipop以上,就是用view自带的方法,否则判断是否实现了NestedScrollingParent接口,去调用接口的方法。
那么具体我们怎么使用这一套机制呢?比如子View这时候我需要通知父view告诉它我有一个嵌套的touch事件需要我们共同处理。那么针对一个只包含scroll交互,它整个工作流是这样的:
一、startNestedScroll
首先子view需要开启整个流程(内部主要是找到合适的能接受nestedScroll的parent),通知父View,我要和你配合处理TouchEvent
二、dispatchNestedPreScroll
在子View的onInterceptTouchEvent或者onTouch中(一般在MontionEvent.ACTION_MOVE事件里),调用该方法通知父View滑动的距离。该方法的第三第四个参数返回父view消费掉的scroll长度和子View的窗体偏移量。如果这个scroll没有被消费完,则子view进行处理剩下的一些距离,由于窗体进行了移动,如果你记录了手指最后的位置,需要根据第四个参数offsetInWindow计算偏移量,才能保证下一次的touch事件的计算是正确的。
如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。
这个函数一般在子view处理scroll前调用。
三、dispatchNestedScroll
向父view汇报滚动情况,包括子view消费的部分和子view没有消费的部分。
如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。
这个函数一般在子view处理scroll后调用。
四、stopNestedScroll
结束整个流程。
整个对应流程是这样
子view | 父view |
---|---|
startNestedScroll | onStartNestedScroll、onNestedScrollAccepted |
dispatchNestedPreScroll | onNestedPreScroll |
dispatchNestedScroll | onNestedScroll |
stopNestedScroll | onStopNestedScroll |
一般是子view发起调用,父view接受回调。
我们最需要关注的是dispatchNestedPreScroll中的consumed参数。
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) ;
它是一个int型的数组,长度为2,第一个元素是父view消费的x方向的滚动距离;第二个元素是父view消费的y方向的滚动距离,如果这两个值不为0,则子view需要对滚动的量进行一些修正。正因为有了这个参数,使得我们处理滚动事件的时候,思路更加清晰,不会像以前一样被一堆的滚动参数搞混。
对NestedScroll的介绍暂时到这里,下一次将讲一下CoordinatorLayout的使用(其中让人较难理解的Behavior对象),以及在SegmentFault Android客户端中的实践。谢谢支持。
Layout_weight是Android开发中一个比较常用的布局属性,在面试中也经常被问到.下面通过实例彻底搞懂Layout_weight的用法.
先看下面的布局代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:background="#44ff0000"
android:gravity="center"
android:text="111111111111"/>
<TextView
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="2"
android:background="#4400ff00"
android:gravity="center"
android:text="2"/>
<TextView
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="3"
android:background="#440000ff"
android:gravity="center"
android:text="3"/>
</LinearLayout>
在水平方向的线性布局中,有三个TextView,layout_width都是0dp,layout_weight分别为1,2,3,所以这三个TextView应该按1:2:3的比例分布,
显示效果:
可以看到,虽然三个TextView是按1:2:3的比例进行分布的,但是第一个TextView却没有和另外两个对齐,这是为什么呢?
仔细看就能发现,虽然三个TextView不是对齐的,但是第一行的文本是对齐的.
我们只需将LinearLayout的baselineAligned属性设置为false即可,baselineAligned表示是否以文本基准线对齐.
这只是对layout_weight属性最基本的用法.假如把第一个TextView的layout_width该为wrap_content,会出现什么情况呢?
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false">
<TextView
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_weight="1"
android:background="#44ff0000"
android:gravity="center"
android:text="111111111111"/>
<TextView
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="2"
android:background="#4400ff00"
android:gravity="center"
android:text="2"/>
<TextView
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="3"
android:background="#440000ff"
android:gravity="center"
android:text="3"/>
</LinearLayout>
看下效果:
发现三个TextView并没有再按照1:2:3的比例进行分配.其实,layout_weight属性是先按控件声明的尺寸进行分配,然后将剩余的尺寸按layout_weight的比例进行分配.
设屏幕总宽度为sum,第一个TextView声明的尺寸为x,那么,
第一个TextView的宽度为: x + (sum - x) * 1/6
第二个TextView的宽度为: (sum - x) * 2/6
第三个TextView的宽度为: (sum - x) * 3/6
为了证实这个结论,接下来继续修改布局,将三个TextView的layout_width都设置为match_parent,把layout_weight分别设置为1,2,2
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false">
<TextView
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_weight="1"
android:background="#44ff0000"
android:gravity="center"
android:text="111111111111"/>
<TextView
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_weight="2"
android:background="#4400ff00"
android:gravity="center"
android:text="2"/>
<TextView
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_weight="2"
android:background="#440000ff"
android:gravity="center"
android:text="3"/>
</LinearLayout>
先看下效果:
看到这个结果可能会感到很诧异,第一个TextView的layout_weight明明比其它两个的小,为什么宽度却比它们大呢?
我们按刚才得出的结论算一下.
依然设屏幕总宽度为sum,由于这三个TextView声明的尺寸都是match_parent,也就是sum,那么,
第一个TextView的宽度为: sum + (sum - 3*sum) * 1/5 = sum*3/5
第二个TextView的宽度为: sum + (sum - 3*sum) * 2/5 = sum*1/5
第三个TextView的宽度为: sum + (sum - 3*sum) * 2/5 = sum*1/5
三个TextView的宽度比例为 3:1:1,所以我们的结论是正确的.
需要注意的是,我们结论所说的剩余的尺寸可能是负的,如 sum - 3*sum,
另外,通过最后这个例子可以看出,并不是layout_weight越大,宽度越大.
Android Layout_weight的深刻理解
首先看一下Layout_weight属性的作用:它是用来分配属于空间的一个属性,你可以设置他的权重。很多人不知道剩余空间是个什么概念,下面 我先来说说剩余空间。
看下面代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<EditText
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="left"
android:text="one"/>
<EditText
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_weight="1.0"
android:text="two"/>
<EditText
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="right"
android:text="three"/>
</LinearLayout>
运行结果是:
看上面代码:只有Button2使用了Layout_weight属性,并赋值为了1,而Button1和Button3没有设置 Layout_weight这个属性,根据API,可知,他们默认是0
下面我就来讲,Layout_weight这个属性的真正的意思:Android系统先按照你设置的3个Button高度 Layout_height值wrap_content,给你分配好他们3个的高度,
然后会把剩下来的屏幕空间全部赋给Button2,因为只有他的权重值是1,这也是为什么Button2占了那么大的一块空间。
有了以上的理解我们就可以对网上关于Layout_weight这个属性更让人费解的效果有一个清晰的认识了。
我们来看这段代码:
<?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:orientation="horizontal" >
<TextView
android:background="#ff0000"
android:layout_width="**"
android:layout_height="wrap_content"
android:text="1"
android:textColor="@android:color/white"
android:layout_weight="1"/>
<TextView
android:background="#cccccc"
android:layout_width="**"
android:layout_height="wrap_content"
android:text="2"
android:textColor="@android:color/black"
android:layout_weight="2" />
<TextView
android:background="#ddaacc"
android:layout_width="**"
android:layout_height="wrap_content"
android:text="3"
android:textColor="@android:color/black"
android:layout_weight="3" />
</LinearLayout>
三个文本框的都是 layout_width=“wrap_content ”时,会得到以下效果
按照上面的理解,系统先给3个TextView分配他们的宽度值 wrap_content(宽度足以包含他们的内容1,2,3即可),然后会把剩下来的屏幕空间按照1:2:3的比列分配给3个textview,所以就 出现了上面的图像。
而当layout_width=“fill_parent”时, 如果分别给三个TextView设置他们的Layout_weight为1、2、2的话,就会出现下面的效果:
你会发现 1的权重小,反而分的多了,这是为什么呢???网上很多人说是当layout_width=“fill_parent”时,weighth 值越小权重越大,优先级越高,就好像在背口诀
一样,其 实他们并没有真正理解这个问题,真正的原因是Layout_width="fill_parent"的原因造成的。依照上面理解我们来分析:
系统先给3个textview分配他们所要的宽度fill_parent,也就是说每一都是填满他的父控件,这里就死屏幕的宽度
那么这时候的剩余空间=1个parent_width-3个parent_width=-2个parent_width (parent_width指的是屏幕宽度 )
那么第一个TextView的实际所占宽度应该=fill_parent的宽度,即parent_width + 他所占剩余空间的权重比列1/5 * 剩余空间大小(-2 parent_width)=3/5parent_width
同理第二个TextView的实际所占宽度=parent_width + 2/5*(-2parent_width)=1/5parent_width;
第三个TextView的实际所占宽度=parent_width + 2/5*(-2parent_width)=1/5parent_width;所以就是3:1:1的比列显示了。
这样你也就会明白为什么当你把三个Layout_weight设置为1、2、3的话,会出现下面的效果了:
第三个直接不显示了,为什么呢?一起来按上面方法算一下吧:
系统先给3个textview分配他们所要的宽度fill_parent,也就是说每一都是填满他的父控件,这里就死屏幕的宽度
那么这时候的剩余空间=1个parent_width-3个parent_width=-2个parent_width (parent_width指的是屏幕宽度 )
那么第一个TextView的实际所占宽度应该=fill_parent的宽度,即parent_width + 他所占剩余空间的权重比列1/6 * 剩余空间大小(-2 parent_width)=2/3parent_width
同理第二个TextView的实际所占宽度=parent_width + 2/6*(-2parent_width)=1/3parent_width;
第三个TextView的实际所占宽度=parent_width + 3/6*(-2parent_width)=0parent_width;所以就是2:1:0的比列显示了。第三个就直接没有空间了。
相关文章
- 下面我们来看一篇关于Android子控件超出父控件的范围显示出来方法,希望这篇文章能够帮助到各位朋友,有碰到此问题的朋友可以进来看看哦。 <RelativeLayout xmlns:an...2016-10-02
- 这篇文章主要介绍了C#窗体布局方式详解的相关资料,需要的朋友可以参考下...2020-06-25
Android开发中findViewById()函数用法与简化
findViewById方法在android开发中是获取页面控件的值了,有没有发现我们一个页面控件多了会反复研究写findViewById呢,下面我们一起来看它的简化方法。 Android中Fin...2016-09-20- 如果我们的项目需要做来电及短信的功能,那么我们就得在Android模拟器开发这些功能,本来就来告诉我们如何在Android模拟器上模拟来电及来短信的功能。 在Android模拟...2016-09-20
- 夜神android模拟器如何设置代理呢?对于这个问题其实操作起来是非常的简单,下面小编来为各位详细介绍夜神android模拟器设置代理的方法,希望例子能够帮助到各位。 app...2016-09-20
- 为了增强android应用的用户体验,我们可以在一些Button按钮上自定义动态的设置一些样式,比如交互时改变字体、颜色、背景图等。 今天来看一个通过重写Button来动态实...2016-09-20
- 如果我们要在Android应用APP中加载html5页面,我们可以使用WebView,本文我们分享两个WebView加载html5页面实例应用。 实例一:WebView加载html5实现炫酷引导页面大多...2016-09-20
- 深入理解Android中View和ViewGroup从组成架构上看,似乎ViewGroup在View之上,View需要继承ViewGroup,但实际上不是这样的。View是基类,ViewGroup是它的子类。本教程我们深...2016-09-20
- 下面我们来看一篇关于Android自定义WebView网络视频播放控件开发例子,这个文章写得非常的不错下面给各位共享一下吧。 因为业务需要,以下代码均以Youtube网站在线视...2016-10-02
- java开发的Android应用,性能一直是一个大问题,,或许是Java语言本身比较消耗内存。本文我们来谈谈Android 性能优化之MemoryFile文件读写。 Android匿名共享内存对外A...2016-09-20
- TextView默认是横着显示了,今天我们一起来看看Android设置TextView竖着显示如何来实现吧,今天我们就一起来看看操作细节,具体的如下所示。 在开发Android程序的时候,...2016-10-02
android.os.BinderProxy cannot be cast to com解决办法
本文章来给大家介绍关于android.os.BinderProxy cannot be cast to com解决办法,希望此文章对各位有帮助呀。 Android在绑定服务的时候出现java.lang.ClassCastExc...2016-09-20- 这篇文章主要介绍了vscode搭建STM32开发环境的详细过程,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-05-02
- 这篇文章主要介绍了Android 实现钉钉自动打卡功能的步骤,帮助大家更好的理解和学习使用Android,感兴趣的朋友可以了解下...2021-03-15
- 下面我们来看一篇关于Android 开发之布局细节对比:RTL模式 ,希望这篇文章对各位同学会带来帮助,具体的细节如下介绍。 前言 讲真,好久没写博客了,2016都过了一半了,赶紧...2016-10-02
- 首先如果要在程序中使用sdcard进行存储,我们必须要在AndroidManifset.xml文件进行下面的权限设置: 在AndroidManifest.xml中加入访问SDCard的权限如下: <!--...2016-09-20
- 下面来给各位简单的介绍一下关于Android开发之PhoneGap打包及错误解决办法,希望碰到此类问题的同学可进入参考一下哦。 在我安装、配置好PhoneGap项目的所有依赖...2016-09-20
- 下面我们一起来看一篇关于 安卓开发之Intent传递Object与List的例子,希望这个例子能够为各位同学带来帮助。 Intent 不仅可以传单个的值,也可以传对象与数据集合...2016-09-20
用Intel HAXM给Android模拟器Emulator加速
Android 模拟器 Emulator 速度真心不给力,, 现在我们来介绍使用 Intel HAXM 技术为 Android 模拟器加速,使模拟器运行度与真机比肩。 周末试玩了一下在Eclipse中使...2016-09-20- 这篇文章主要为大家详细介绍了php微信公众账号开发之五个坑,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2016-10-02