前言:
我们安卓实验中有一个是制作一个简单的浏览器,其中就涉及到了一个网页的前进与回退。我们都知道Android系统为我们提供了一个基于WebKit内核的WebView控件,其中自带了一些方法。
比如:
canGoBack() 用于判断是否能够进行后退操作
Gets whether this WebView has a back history item.
canGoForward() 用于判断是否能够进行前进操作
Gets whether this WebView has a forward history item.
goBack()用于回退到上一个页面
Goes back in the history of this WebView.
goForward ()用于前进一个页面
Goes forward in the history of this WebView.
这些官方已经封装好的方法使用固然很方便,但是,但是,但是,今天我们将从底层(不使用Google以及Java提供的现成API),使用数据结构和算法的知识来实现一下WebView控件中以上提及的这四个函数。
知识储备:
一、单链表
谈到单链表又不得不提到线性表。线性表是具有相同特性数据元素的一个有限序列。它有顺序存储以及链式存储两种存储结构,前者叫顺序表(数组),后者叫链表。
线性表的特征:
1、只有一个表头元素,只有一个表尾元素
2、表头元素没有前驱,表尾元素没有后继
3、除了表头和表尾元素之外,其他元素有且只有一个直接前驱,有且只有一个直接后继
链表又有它自己的特征:
1、它不支持随机访问,即给定一个位置后,能够直接找到这个位置
2、理想状态下,一个节点应该全部用于存储数据,而在链表中,节点被分为了数据域和指针域,因此,节点的利用率降低了。
3、链表支持动态分配
链表又分为:单链表、循环单链表、双链表、循环双链表、静态链表,我们今天所使用的是最简单的单链表。
在单链表中,又分为:带头节点的单链表与不带头节点的单链表。我们今天所使用的是不带头节点的单链表。
二、栈
栈是一种只能在一端进行插入或删除操作的线性表。栈遵循后进先出(Last In First Out)的原则。栈也分为顺序栈和链式栈。我们今天所使用的是链式栈,即用单链表构成栈结构,栈中的每一个元素都是单链表的节点。
图1:链式栈结构 A为栈顶,N为栈底
实现过程
一、链式栈的代码实现(为了可移植性考虑,全部使用泛型)
1、节点
public class Node<T>
{
private T data;
private Node next;
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
在节点类中,显然有两个域,一个数据域,另外一个指针域。这里的指针域就是指向下一个Node元素的引用
2、单链表
//不带头节点的单链表
public class LinkedList<T>
{
private Node<T> head;
private int size;
public LinkedList()
{
this.size = 0;
head = null;
}
//头插法
public boolean insert(T data)
{
Node<T> node = new Node<T>();
node.setData(data);
//第一个节点
if(head == null)
{
node.setNext(null);
head = node;
}
else
{
//不是第一个节点
node.setNext(head);
}
head = node;
size++;
return true;
}
//删除链表第i个位置的元素
public T delete(int position)
{
if(position < 1 || head == null)
{
return null;
}
Node<T> p = head;
//如果删除的是第一个元素
if(position == 1)
{
T data = p.getData();
head = p.getNext();
size--;
return data;
}
for(int i = 1 ; i < position-1 ; i++)
{
p = p.getNext();
}
T data = (T) p.getNext().getData();
p.setNext(p.getNext().getNext());
size--;
return data;
}
//获取某一个位置的元素
public T getElement(int position)
{
if(position < 1 || head == null)
{
return null;
}
Node<T> p = head;
for(int i = 1 ; i < position ; i++)
{
p = p.getNext();
}
return p.getData();
}
public boolean isEmpty()
{
return size == 0 ? true : false;
}
public void display()
{
Node<T> p = head;
System.out.println("-------");
while(p != null)
{
System.out.println(p.getData());
p = p.getNext();
}
System.out.println("---END----");
System.out.println();
}
}
这里应该算是这个案例的第一个
重点了吧,涉及了数据结构中单链表的基本操作。为了后续实现方便,我使用了不带头结点的单链表。
变量:
Node<T> head ----- 头指针,指向单链表的第一个节点
int size ----单链表的大小
方法:
boolean insert(T data) 单链表的插入,为了适应栈结构的特点,使用头插法
T delete(int position) 删除单链表第position位置的元素,并返回该元素
T getElement(int position) 获取单链表第position位置的元素,并返回
boolean isEmpty()判断单链表是否为空
void display()打印单链表的内容
3、栈结构
public class Stack<T>
{
LinkedList<T> list;
int cSize;
public Stack()
{
this.list = new LinkedList<T>();
cSize = 0;
}
public boolean empty()
{
return cSize == 0 ? true : false;
}
public T pop()
{
if(empty()) return null;
T e = list.delete(1);
cSize--;
return e;
}
public boolean pull(T element)
{
list.insert(element);
cSize++;
return true;
}
public int getSize()
{
return this.cSize;
}
public void display()
{
list.display();
}
}
变量:
LinkedList<T> list ---- 单链表的对象
int cSize ---栈的大小
方法:
boolean empty() 判断栈是否为空
T pop() 出栈
boolean pull(T element) 把元素element入栈
int getSize() 获取栈的大小
void display() 打印栈中内容
注:具体的实现过程可以看代码,本文的重点并不是说这些基本操作的实现过程而是如何应用这些结构来达到我们所需要的效果
二、浏览器前进与后退功能的实现逻辑
这里就是整个案例的核心,有了之前数据结构的基础做铺垫,剩下的就是我们自己的逻辑了,要把这个结构利用好~
整体思路是这样的:根据栈结构后进先出的顺序,我们很容易想到,可以把用户当前浏览的网页入栈,当用户在新界面点击回退按钮的时候,直接让栈顶元素出栈,让WebWiew加载,不就实现了后退功能了吗?前进的功能也类似,于是乎,我们想到用两个栈分别存储在用户当前浏览网页之前/之后的所有页面地址。如下图所示
图2:理想的结构(BackStack回退栈,ForwardStack前进栈)
有了整体框架结构了,剩下的就是具体的实现了,重点是要获取到不论是用户从地址栏输入的,还是页面点击的URL地址。我便开始在与WebView的文档中搜寻,最初发现的是在WebViewClient类(后面会说)下的shouldOverrideUrlLoading (WebView view, String url)方法
但是在实际操作中发现一个问题,这个方法是会截取URL没错,但是并不是我每次加载网页的时候都被调用,在我这里是用户输入、网页点击、点击回退按钮的时候会被调用,而点击前进按钮的时候并不会。后来,我又发现了onPageStarted方法
第一句话便是:告诉应用程序有页面已经开始加载了。那是不是说对于每一个要加载的URL(无论是否加载过)都会被调用呢?经过实验,确实如此,最重要的一个方法被我们找到了!
接下来的一个问题是,既然这个方法每次都会被调用,那我们如何让应用程序知道这个页面是新加载的?还是后退的?还是前进的呢?于是我就想把每一个待加载的URL封装成一个对象,在网页加载时,判断其中的特定值来进行确定。URL类的Java代码如下:
public class URL
{
private String address;
private boolean isBack;
private boolean isForward;
public URL(String url)
{
this.address = url;
this.isBack = false;
this.isForward = false;
}
public void setBack(boolean value)
{
this.isBack = value;
}
public void setForward(boolean value)
{
this.isForward = value;
}
public boolean isBack() {
return isBack;
}
public boolean isForward() {
return isForward;
}
public String getAddress() {
return address;
}
}
变量:
String address---网页地址
boolean isBack---是否是回退操作
boolean isForward---是否是前进操作
方法:
唯一要说明的是构造方法,在构造一个URL对象时,我把isBack与isForward都设置成false,表示这个页面是新加载的。
所以,不外乎以下这几种情况
isBack = false isForward = false 新加载
isBack = true isForward = false 后退操作
isBack = false isForward = true 前进操作
isBack = true isForward = true 不可能存在的操作,肯定是出错了
OK,有了URL类之后,我们就可以在onPageStarted方法中进行判断加载的URL的类型了。还有一个问题是,这样做固然理论上是基本实现了,但是在实际中,我们会发现在回退或者前进的时候,要连续按两次按钮才能够起作用,这个是为什么呢?因为我们在加载新网页的时候,就直接将其压入了回退栈(BackStack)的栈顶,导致当用户点击回退按钮的时候,依然是上次的那个页面。为了解决这个问题,我使用另外一个URL型变量pre来存储用户浏览的当前页面,但是在加载时不压入回退栈(BackStack)中,只有当下一次有其他页面加载的时候再把pre压入回退栈(BackStack)中。下面用图说明
假设用户正在浏览www.abc.com,这时pre变量也为www.abc.com
用户此时在浏览www.def.com。我们在加载www.def.com的时候,把之前pre变量中存储的www.abc.com压入回退栈(BackStack)中,再把pre变量赋值为www.def.com
接下来的问题就比较好处理了,我们在MainActivity中定义一个静态的URL对象obj,然后在onPageStarted方法中判断这个obj变量是否为null,为null表示是一个全新的加载,如果不为空,表示是后退或者前进操作的其中一个,由pre对象是否为空,判断这是不是应用程序开启时加载的第一个网页。
在onPageStarted方法代码如下:
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
textView.setText(url);
if(MainActivity.obj == null)
{
//System.out.println("NULL");
URL nurl = new URL(url);
if(pre != null)
{
backStack.pull(pre);
}
pre = nurl;
}
else
{
//System.out.println("1:BACK---"+MainActivity.obj.isBack()+" FORWARD---"+MainActivity.obj.isForward());
pre = MainActivity.obj;
if(MainActivity.obj.isBack())
{
MainActivity.obj.setBack(false);
}
if(MainActivity.obj.isForward())
{
MainActivity.obj.setForward(false);
}
//System.out.println("2:BACK---"+MainActivity.obj.isBack()+" FORWARD---"+MainActivity.obj.isForward());
}
MainActivity.obj = null;
}
我们主要控制新网址加载时回退栈(BackStack)的压栈操作,还有一个是在确定obj对象不为空时,判断是前进还是后退的网站,确定之后将为ture的值设置成false,回到初始状态,并把obj置空。
现在回到上面说的shouldOverrideUrlLoading方法,我在代码中重写了这个方法,如果不重写的话会导致网页会在系统的浏览器加载,而不是我们定义的WebView。最后说一下返回值吧,官方的文档中说:
return true means the host application handles the url, while return false means the current WebView handles the url
简单来说返回true表示我们的应用程序拿到这个url的控制权,返回false由WebView自行处理。在这个案例中,由于我们并没有做什么其他操作,true和false没有明显区别。
然后是那个WebViewClient类,这个类可以配置WebView,比如我们要对url进行处理的话,就需要继承这个类。
Client.java代码内容如下
import android.content.Context;
import android.graphics.Bitmap;
import android.webkit.WebResourceRequest;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.TextView;
import com.example.administrator.webstackdemo.DS.Stack;
/**
* Created by Martin Huang on 2017/4/28.
*/
public class Client extends WebViewClient
{
private Stack<URL> backStack;
private TextView textView;
private URL pre;
public Client(Stack<URL> backStack , TextView textView)
{
this.backStack = backStack;
this.textView = textView;
pre = null;
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
textView.setText(url);
if(MainActivity.obj == null)
{
//System.out.println("NULL");
URL nurl = new URL(url);
if(pre != null)
{
backStack.pull(pre);
}
pre = nurl;
}
else
{
//System.out.println("1:BACK---"+MainActivity.obj.isBack()+" FORWARD---"+MainActivity.obj.isForward());
pre = MainActivity.obj;
if(MainActivity.obj.isBack())
{
MainActivity.obj.setBack(false);
}
if(MainActivity.obj.isForward())
{
MainActivity.obj.setForward(false);
}
//System.out.println("2:BACK---"+MainActivity.obj.isBack()+" FORWARD---"+MainActivity.obj.isForward());
}
MainActivity.obj = null;
}
}
最后是MainActivity.java
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import com.example.administrator.webstackdemo.DS.Stack;
import java.net.*;
public class MainActivity extends AppCompatActivity {
//后退栈
private Stack<URL> backStack;
//前进栈
private Stack<URL> forwardStack;
//后退按钮
private Button backButton;
//前进按钮
private Button forwardButton;
//假设这个是显示网页的地方
private WebView stage;
//网址输入框
private EditText editor;
// private String currentURL;
private Client client;
public static URL obj = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//初始化两个栈的数据结构
backStack = new Stack<URL>();
forwardStack = new Stack<URL>();
//获取两个按钮实例
backButton = (Button)findViewById(R.id.backButton);
forwardButton = (Button) findViewById(R.id.forwardButton);
//获取输入框实例
editor = (EditText) findViewById(R.id.url);
//获取显示区域实例
stage = (WebView) findViewById(R.id.content);
WebSettings settings = stage.getSettings();
settings.setJavaScriptEnabled(true);
stage.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);
client = new Client(backStack,editor);
stage.setWebViewClient(client);
//初始化,设置主页为www.smxy.cn
stage.loadUrl("http://www.smxy.cn");
// currentURL = "http://www.smxy.cn";
//editor.setText(currentURL);
}
public void load(View view)
{
/*
获取输入的网址内容
如果为空,结束
*/
String url = editor.getText().toString();
if(url.trim().equals(""))
{
Toast.makeText(MainActivity.this,"Not null!",Toast.LENGTH_SHORT).show();
return;
}
if(url.indexOf("http://") == -1)
{
url = "http://"+url;
}
stage.loadUrl(url);
}
public void goBack(View view)
{
/*
回退操作
*/
if(backStack.empty())
{
Toast.makeText(MainActivity.this,"End",Toast.LENGTH_SHORT).show();
return;
}
forwardStack.pull(new URL(editor.getText().toString()));
URL turl = backStack.pop();
turl.setBack(true);
obj = turl;
stage.loadUrl(turl.getAddress());
}
public void goForward(View view)
{
/*
前进操作,与回退操作类似
*/
if(forwardStack.empty())
{
Toast.makeText(MainActivity.this,"End",Toast.LENGTH_SHORT).show();
return;
}
backStack.pull(new URL(editor.getText().toString()));
URL turl = forwardStack.pop();
turl.setForward(true);
obj = turl;
stage.loadUrl(turl.getAddress());
}
}
这里的逻辑是
1、用户点击回退,判断回退栈(BackStack)是否为空,为空结束
否则把当前网站压入前进栈(ForwardStack)中
接着从回退栈(BackStack)中取出栈顶元素,设置其isBack为true,并且赋值给obj对象,然后进行加载
2、用户点击前进的逻辑与后退类似。
3、当用户从地址栏输入时,判断输入是否为空,为空弹Toast提示,不为空则判断是否有http前缀,没有的话加上,并加载页面。如果没有http前缀的话,页面是加载不出来的。
附上MainActivity的布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.example.administrator.webstackdemo.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/component1"
android:orientation="horizontal"
>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/url"
android:inputType="textLongMessage"
android:singleLine="true"
android:maxLength="2083"
/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="GO"
android:onClick="load"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_below="@id/component1"
android:id="@+id/component2"
>
<WebView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/content"
>
</WebView>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/component3"
android:orientation="horizontal"
android:layout_alignParentBottom="true"
>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Back"
android:id="@+id/backButton"
android:layout_weight="1"
android:onClick="goBack"
/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Forward"
android:id="@+id/forwardButton"
android:layout_weight="1"
android:onClick="goForward"
/>
</LinearLayout>
</RelativeLayout>
项目的结构图如下
运行效果如下
源码地址: