Contents
  1. 1. 结构
  2. 2. 初始化
  3. 3. 检测是否允许下拉刷新
  4. 4. 刷新过程
  5. 5. 封装WebView对外接口
  6. 6. 如何使用

之前项目中需要用到下拉刷新功能,虽然Android在SwipeRefreshLayout中集成了下拉刷新的功能,但是看起来并不那么舒服;加上我一贯不喜欢用第三方的库来完成本来可以自己实现的功能,所以,就自己开始研究,如何才能不借助任何工具,实现下拉刷新呢?

网上还是有不少布道者给出了解决方案,然后我就仿照着这篇Blog:Android下拉刷新完全解析,教你如何一分钟实现下拉刷新功能 ,实现了给WebView加上下拉刷新的功能。先来看一下最终的效果图:



大致的实现原理可以参见原作者的博文。原作者是实现ListView的下拉刷新,我也是仔细阅读了作者的源码,读懂了他的实现原理,然后加以精华和修改,完成了自己的功能实现。因为之前的功能和我们自己的项目耦合性太高,所以我特地花了2天时间,把下拉webview的功能分离出来,形成了一个单独的库,已经上传到我的Github, 感兴趣的同学可以直接使用,同时欢迎Star和Fork,我也会不断完善相关的功能。

这篇博文就来分析一下我的源码,看看究竟是如何在webview中实现下拉刷新功能的。
因为源码比较长,而且后续还会不断更新,所以我就不在这里贴出来了,大家可以参照这里并照着帖子来研究下。

结构

这里盗一张图,其实这里的结构和郭林的Blog中的是一样的,都是隐藏在主题之上的,不过他的主体是ListView,而我这里是WebView而已。



这里附上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
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
<?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">

<!-- WebView container layout -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/base_web_view_header_layout">

<!-- webViewContainer-->
<RelativeLayout
android:id="@+id/base_web_view_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

</RelativeLayout>

<!-- header layout-->
<LinearLayout
android:id="@+id/base_web_view_header_layout"
android:layout_width="120dp"
android:layout_height="60dp"
android:layout_centerHorizontal="true"
android:layout_marginTop="-60dp"
android:orientation="horizontal">

<!-- left area : arrow image-->
<RelativeLayout
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_weight="3">
<ImageView
android:id="@+id/pull_to_refresh_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@mipmap/arrow"
android:visibility="visible"/>

<!-- loading gif -->
<pl.droidsonroids.gif.GifTextView
android:id="@+id/pull_to_refresh_progress_bar"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_centerInParent="true"
android:background="@drawable/pull_to_refresh_progress"
android:visibility="gone"/>
</RelativeLayout>

<!-- right area : text view -->
<LinearLayout
android:layout_width="0dip"
android:layout_height="60dip"
android:layout_weight="12"
android:orientation="horizontal" >
<TextView
android:id="@+id/pull_to_refresh_description"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:textColor="@color/pull_to_refresh_text_color"
android:textAlignment="center"
android:text="@string/pull_to_refresh" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>

这里可以看到其实整个是一个RelativeLayout,然后顶部的header layout是一个水平方向的LinearLayout
头部左侧区域放的是下拉的箭头,当达到下拉刷新的极限长度时,做一个旋转的动作,把下拉的箭头旋转成向上的;同时左侧还有一个GIF的旋转图片,当处于刷新状态时,隐藏下拉箭头图片,同时显示旋转的GIF动画。 这里关于GIF图片我们使用了一个第三方的库
头部右侧区域放的是TextView,用于显示当前的状态,例如“继续下拉刷新”,“释放立即刷新”等,用于告知用户当前的状态。

在头部下方,是我们的WebView主体内容。可以看到我并没有在XML文件中静态地添加WebView,添加WebView的方法是通过代码实现的,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Description: init the webview object
* We add webView object into view with codes but not in XML, the benefits is that we can release
* the memory more deeply and clearly when we destroy webview's instance.
* Created by Michael Lee on 9/21/16 10:47
* @param aContext current context
*/
private void initWebView(Context aContext) {
RelativeLayout webview_container = (RelativeLayout) findViewById(R.id.base_web_view_container);
web_view_ = new MyWebView(aContext,null);
web_view_.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
web_view_.setOnTouchListener(this);

webview_container.addView(web_view_);
}

这样做的目的是,如果静态的添加WebView,则不能在Activity destroy时完全clear掉WebView占用的内存资源,而通过代码添加则可以很好的释放资源。但是目前1.0.0版本里面没有添加释放资源的接口,后续打算补充完整。

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Description: see {@link LinearLayout#onLayout(boolean, int, int, int, int)}
* After views are initialized, get web_view's location in screen to check if able to pull_to_
* refresh. And these codes only be execute once.
* Created by Michael Lee on 9/21/16 15:46
*/
@Override
public void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed,l,t,r,b);
if (changed && !load_once_) {
// confirm the WebView's original location
web_view_.getLocationOnScreen(location_array_);
original_location_y_ = location_array_[1];

header_layout_params_ = (MarginLayoutParams) header_.getLayoutParams();
hide_header_height_ = -header_.getHeight();

load_once_ = true;
}
}

这里需要记录一下初始化之后,WebView的初始坐标,因为当我们判断是否可以执行下拉刷新动作的时候,需要先判断下当前WebView的坐标是否大于初始坐标;
同时,因为onLayout是在界面初始化时被调用,所以我们需要把header部分隐藏起来,做法就是通过改变头部的MarginLayoutParams,而且这段代码仅需要执行一次,所以通过变量load_once来控制。

检测是否允许下拉刷新

下拉刷新仅当控件当前已经处于顶部时,才有意义。郭林的博文中因为是采用的ListView,所以他可以通过判断第一个item来判断是否处于List的顶部,从而来判断是否允许下来刷新,所以我们需要重写WebView中的onScrollChanged方法来设置can_pull_to_refresh_参数表示当前是否处于顶端从而可以下拉刷新

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
/**---------------------------------------------------------------------------------------------
* Description: mywebview extends {@link WebView}
* override onScrollChanged method to set if we should allow pull-to-refresh, cause when user
* is navigating the web page, we should NOT show the pull-to-refresh header.
*--------------------------------------------------------------------------------------------*/
public class MyWebView extends WebView {
/**
* Description: Default Constructor
* Created by Michael Lee on 9/21/16 17:13
* @param context context
*/
public MyWebView(Context context) {
this(context,null);
}

/**
* Description: copy constructor.
* Do NOT invoke this(context, attrs,0), cause if style type was 0, would not response click
* event, and if type is 'com.android.internal.R.attr.webViewStyle', there would be on compile
* error:
* You cannot access id's of com.android.internal.R at compile time, but you can access the
* defined internal resources at runtime and get the resource by name.
* You should be aware that this is slower than direct access and there is no guarantee.
* Created by Michael Lee on 9/21/16 17:13
* @param context context
* @param attrs attributes Set
*/
public MyWebView(Context context, AttributeSet attrs) {
this(context, attrs, Resources.getSystem().getIdentifier("webViewStyle", "attr", "android"));
}

/**
* Description: Copy Constructor
* Created by Michael Lee on 9/21/16 17:13
* @param context context
* @param attrs attributes set
* @param defStyle sytle type
*/
@SuppressLint({"SetJavaScriptEnabled"})
public MyWebView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// Set WebView client
setWebViewClient(new MyWebViewClient());
// set JS
getSettings().setJavaScriptEnabled(true);
// Block press and hold event
this.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
return true;
}
});
}

/**
* Description: see {@link WebView#onScrollChanged}
* When webview scrolling, keep the progress bar on the top.
* Created by Michael Lee on 9/21/16 17:13
* @param l Current horizontal scroll origin
* @param t current vertical scroll origin
* @param oldl Previous horizontal scroll origin
* @param oldt Previous vertical scroll origin
*/
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
// if current vertical scroll origin is not 0, CAN NOT pull to refresh
can_pull_to_refresh_ = (t == 0);
super.onScrollChanged(l, t, oldl, oldt);
}
}

这部分就是我们重写的WebView,其中还设置了自定义的WebViewClient用来记录当前浏览的URL方便刷新,以及页面加载完成后重新隐藏下拉头的部分。

可以看到在Override的onScrollChanged方法中,当t==0时,我们就认为当前已经处于顶部了,就把can_pull_to_refresh_设置为TRUE这样就允许下拉刷新了。而当t!=0时,我们知道当前用户正处于正常浏览页面的过程中,就屏蔽掉下拉刷新。

刷新过程

整个刷新过程是在onTouch方法中实现的,所以WebView要implements View.OnTouchListener接口。这里基本上和郭林的实现是一样的,只不过我这里做了一些简化。

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
/**
* Description: View.OnTouchListener
* Created by Michael Lee on 9/21/16 16:23
* @param v touched view
* @param event motion event
* @return 'false' means do nothing, 'true' means do custom actions.
*/
@Override
public boolean onTouch(View v, MotionEvent event) {
web_view_.getLocationOnScreen(location_array_);
// we should reduce the original coordinate by 1, cause when convert float to int, there will
// be 1 pix error.
if ((original_location_y_ - 1) <= location_array_[1] && can_pull_to_refresh_) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
pressed_coords_y = event.getRawY();
break;

case MotionEvent.ACTION_MOVE:
float y_move = event.getRawY();
int distance = (int) (y_move - pressed_coords_y);

if (distance <= 0 && header_layout_params_.topMargin <= hide_header_height_) {
return false;
}

// distance is too small
if (distance < touchSlop) {
return false;
}

if (current_status_ != STATUS_REFRESHING) {
if (header_layout_params_.topMargin > 0) {
current_status_ = STATUS_RELEASE_TO_REFRESH;
} else {
current_status_ = STATUS_PULL_TO_REFRESH;
}
// add the header's top margin
header_layout_params_.topMargin = (distance / 5 * 2) + hide_header_height_;
// LogUtils.d(TAG,"header layout's top margin is : " +
// header_layout_params_.topMargin);
header_.setLayoutParams(header_layout_params_);
}
break;

case MotionEvent.ACTION_UP:
default:
// when release the finger
if (current_status_ == STATUS_RELEASE_TO_REFRESH) {
refreshingAction();
} else {
// hide header
hideHeader();
}
break;
}

if (current_status_ != STATUS_REFRESH_FINISHED) {
updateHeaderView();
last_status_ = current_status_;
return true;
}
}
return false;
}

封装WebView对外接口

最后这里封装了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
/**
* Description: this method is 'public' for outer class to load url into progressWebView
* manually.
* In some case, outer class may want to load url into webView.
* For example, when login success, the inside activity should refresh the url before goto login.
* Created by Michael Lee on 9/21/16 10:41
* @param url url for navigating
*/
public void loadUrl(String url) {
if (web_view_ == null && current_context_ != null) {
Log.d(TAG,"current web view has not be init, do it now");
initWebView(current_context_);
}
if (url != null && !url.equals("")) {
web_view_.loadUrl(url);
} else {
Log.e(TAG,"Url navigating is NULL or empty");
}
}

/**
* Description: webView's canGoBack
* Created by Michael Lee on 9/21/16 15:26
* @return can go back? true = yes, false = no.
*/
public boolean canGoBack() {
return web_view_.canGoBack();
}

/**
* Description: webView's goBack
* Created by Michael Lee on 9/21/16 15:30
*/
public void goBack() {
if (canGoBack()) {
web_view_.goBack();
} else {
Log.d(TAG,"Can NOT go back anymore");
}
}

如何使用

关于源码的介绍基本就这些了,如果有不同的可以在页面下留言,详细的中文注释的源码可以去读一读郭林的Blog

关于如何使用我的库,我会在GitHub的页面中进行详细的说明,有兴趣的同学欢迎Star和Fork,再次感谢。

我的Github页面:https://github.com/lipeng1667/PullToRefreshWebView

Contents
  1. 1. 结构
  2. 2. 初始化
  3. 3. 检测是否允许下拉刷新
  4. 4. 刷新过程
  5. 5. 封装WebView对外接口
  6. 6. 如何使用