使用棧結構簡易實現瀏覽器的後退與前進功能(以Android為例)
前言:
我們安卓實驗中有一個是製作一個簡單的瀏覽器,其中就涉及到了一個網頁的前進與回退。我們都知道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>
專案的結構圖如下
執行效果如下
原始碼地址: