articulo creado por http://danielme.com/
Android WebView: incrustar un navegador en
nuestras apps
VERSIONES
1. 19/05/2012 (Primera
publicación)
2. 28/10/2012:
·
Añadida nota a la introducción sobre la versión de WebKit
·
Tratamiento de favicon (sólo en el código de GitHub)
3. 06/12/2012:
·
Integración del favicon con el código de ejemplo del tutorial
·
Comprobación de conectividad (anteriormente incluído en los comentarios)
·
Solucionado el problema que no mostraba el teclado virtual en los input
·
Gestión de rotaciones
·
Launcher icon personalizado
4. 16/02/2013:
·
Descargas asíncronas
·
Activación de plugins
·
Actualización del entorno de pruebas
·
Enlaces a los tips Android relacionados
5. 17/11/2013:
·
Código revisado y probado con Android 4.3 y 4.4
·
Mejorado historial (sólo en demo de GitHub)
·
Añadida descripción de novedades de WebView en Android 4.4
·
Sólo se ofrece como código de ejemplo la demo completa en GitHub para
facilitar el mantenimeinto y actualización del artículo.
NOTA
Este artículo explica la mayor parte de una demo más completa publicada en GitHub.
Android SDK proporciona un widget denominado WebView para renderizar páginas web, ya
sea obteniéndolas través de una URL o bien recibiendo el html directamente
desde una Activity.
WebView está basado en el proyecto Open Source WebKit,
que incluye un motor de renderizado de html y un intérprete de javascript.
WebKit es utilizado en numerosas aplicaciones y es la base de navegadores
como Safari, Chromiun – Google Chrome oMidori.
En este artículo se va a exponer de forma práctica cómo incrustar y
utilizar en una app un navegador web; WebView, además de renderizar html, es
muy potente y altamente configurable y proporciona numerosos métodos para
controlar la navegación, gestionar cookies, procesar los errores, etc. El uso
que veremos será básico y orientado a la navegación web, ya que con WebView se
podría incluso realizar las interfaces gráficas de nuestra app en html y
utilizar javascript para ejecutar código Android al estilo de PhoneGap.
Para este uso he escrito otroartículo.
Nota: Para este tutorial se usará la API 7 (Android 2.1), pero la versión de
WebKit dependerá de la versión de Android del dispositivo en el que se ejecute
la demo. Para conocerla, bastará con ver el user-agent con el que se identifica
el widget WebView o, más fácil todavía, visitar http://jimbergman.net/webkit-version-in-android-version/ .
Entorno de pruebas:
Requisitos: Conocimientos muy básicos de Android y Eclipse ADT.
Ejemplo básico
No nos complicamos la vida y creamos un proyecto de tipo Android con el
asistente. Voy a usar la API 7 (2.1) para conseguir la mayor compatibilidad posible y que es más que
suficiente para el propósito de este mini-tutorial.
WebView se utiliza como cualquier otro widget, lo incluímos en el main.xml creado
por el asistente (y eliminamos también el Hello World):
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<?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="fill_parent"
android:orientation="vertical" >
<WebView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/webkit"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
</LinearLayout>
|
En nuestra Activity, establecemos la página que queremos mostrar:
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
|
package com.danielme.blog.android.webviewdemo;
import android.app.Activity;
import android.os.Bundle;
import android.webkit.WebView;
public class WebViewdemoActivity extends Activity
{
private WebView
browser;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
browser =
(WebView)findViewById(R.id.webkit);
//habilitamos
javascript y el zoom
browser.getSettings().setJavaScriptEnabled(true);
browser.getSettings().setBuiltInZoomControls(true);
//habilitamos los
plugins (flash)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO)
{
webview.getSettings().setPluginState(PluginState.ON);
}
else
{
//IMPORTANTE!!
este método ha sido eliminado en Android 4.3
//por
lo que si lo necesitamos para mantener la compatibilidad
//hacia
atrás hay que compilar el proyecto con Android 4.2 como máximo
webview.getSettings().setPluginsEnabled(true);
}
browser.loadUrl("http://danielme.com");
}
}
|
También necesitaremos en el AndroidManifest.xml solicitar
permiso para la conexión a Internet.
7
|
<uses-permission android:name="android.permission.INTERNET"/>
|
Veamos que tal se renderiza WordPress en un móvil ejecutando el proyecto
en el emulador de ADT para la API 7.
Si se pulsa un enlace, Android lo abrirá con la aplicación por defecto
para tal fin, esto es, el propio navegador. Si queremos permanecer en nuestra
app hay que especializar la clase WebViewClient y sobreescribir el métodoshouldOverrideUrlLoading para que
devuelva false. Puesto que en este método se recibe como parámetro la url
solicitada se podría utilizar también para filtrar las direcciones a las que se
permite el acceso.
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
|
package com.danielme.blog.android.webviewdemo;
import android.app.Activity;
import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;
public class WebViewdemoActivity extends Activity
{
private WebView
browser;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
browser =
(WebView)findViewById(R.id.webkit);
//habilitamos
javascript y el zoom
browser.getSettings().setJavaScriptEnabled(true);
browser.getSettings().setBuiltInZoomControls(true);
//habilitamos
los plugins (flash)
browser.getSettings().setPluginsEnabled(true);
browser.setWebViewClient(new WebViewClient()
{
//
evita que los enlaces se abran fuera nuestra app en el navegador de android
@Override
public boolean shouldOverrideUrlLoading(WebView
view, String url)
{
return false;
}
});
}
}
|
Barra de progreso
En el ejemplo anterior, el usuario no puede saber si una página se está
cargando hasta que WebView la muestre, creando la sensación de que el navegador
no responde. Afortunadamente es fácil proporcionar una barra de progreso con el
widget ProgressBar y haciendo uso de la
clase WebChromeClient para saber la evolución
del proceso de carga de la página. Esta barra la ocultaremos una vez haya
finalizado la carga de la página para ahorrar espacio en pantalla.
Primero añadimos la barra a nuestra interfaz, por ejemplo justo arriba
del navegador.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
<?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="fill_parent"
android:orientation="vertical" >
<ProgressBar
android:id="@+id/progressbar"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_margin="3dp"
android:visibility="gone"/>
<WebView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/webkit"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
</LinearLayout>
|
Añadimos el código que gestionará la barra:
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
|
package com.danielme.blog.android.webviewdemo;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ProgressBar;
public class WebViewdemoActivity extends Activity
{
private WebView browser;
private ProgressBar
progressBar;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
browser =
(WebView)findViewById(R.id.webkit);
//habilitamos
javascript y el zoom
browser.getSettings().setJavaScriptEnabled(true);
browser.getSettings().setBuiltInZoomControls(true);
//habilitamos
los plugins (flash)
browser.getSettings().setPluginsEnabled(true);
browser.setWebViewClient(new WebViewClient()
{
//
evita que los enlaces se abran fuera nuestra app en el navegador de android
@Override
public boolean shouldOverrideUrlLoading(WebView
view, String url)
{
return false;
}
});
progressBar =
(ProgressBar) findViewById(R.id.progressbar);
browser.setWebChromeClient(new WebChromeClient()
{
@Override
public void onProgressChanged(WebView view, int progress)
{
progressBar.setProgress(0);
progressBar.setVisibility(View.VISIBLE);
WebViewdemoActivity.this.setProgress(progress
* 1000);
progressBar.incrementProgressBy(progress);
if(progress
== 100)
{
progressBar.setVisibility(View.GONE);
}
}
});
}
}
|
El resultado:
Lamentablemente la barra de progreso no permite hacer demasiadas
florituras, así que hay que ingeniárselas para personalizar y mejorar su aspecto.
Personalmente yo no me complicaría tanto y superpondría sobre la barra un
TextView, ubicando ambos widgets en un FrameLayout. Es la soución por la que he
optado en el ejemplo que he publicado en GitHub.
También existe la posibilidad de utilizar la barra de progreso propia de
Android ubicada debajo de la barra de título de la aplicación tal y como se
hace en el navegador de Android. No obstante, esta solución es menos flexible y
sólo es válida si nuestra app utiliza la barra de título por defecto, algo que
hoy en día no es demasiado habitual.
“Title” de la página
Con WebChromeClient es posible recuperar el title de la página que se
esté cargando en cuanto este sea leído gracias al método onReceivedTitle. Esto permite, por ejemplo,
mostrarlo en la barra de la aplicación al estilo de los navegadores
“tradicionales”.
//la implementación de este
método permite para mostrar el title de la página en la barra de título de la
aplicación
@Override
public void onReceivedTitle(WebView view, String title)
{
WebViewdemoActivity.this.setTitle("WebView
demo: " + WebViewdemoActivity.this.browser.getTitle());
}
|
Barra de navegación
Sigamos completando nuestro navegador con una barra de navegación que
constará de los siguientes elementos:
·
Campo de texto para introducir una dirección url
·
Botón “Ir” para cargar la url del cuadro de texto
·
Botón de navegación hacia atrás. Sólo se mostrará cuando haya una página
anterior a la que volver.
·
Botón de navegación hacia adelante. Sólo se mostrará cuando haya al
menos una página por delante en el historial.
·
Botón para cancelar la carga de la página. Habilitado mientras se esté
cargando una página.
Usando un RelativeLayout y el editor visual de ADT añadimos los
elementos anteriores a nuestro Layout. Excepto el botón de “Ir” que se mostrará
siempre, todos los demás estarán por defecto deshabilitados y se controlarán
programáticamente
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
|
<?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="fill_parent"
android:orientation="vertical" >
<ProgressBar
android:id="@+id/progressbar"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_margin="3dp"
android:visibility="gone"/>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<EditText
android:id="@+id/url"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_toLeftOf="@+id/buttonIr"
android:ems="10"
android:inputType="textUri"
android:selectAllOnFocus="true">
</EditText>
<Button
android:id="@+id/buttonIr"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/url"
android:layout_alignBottom="@+id/url"
android:layout_alignParentRight="true"
android:onClick="ir"
android:text="@string/ir" />
<Button
android:id="@+id/buttonAnt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_below="@+id/url"
android:enabled="false"
android:onClick="anterior"
android:text="@string/anterior" />
<Button
android:id="@+id/buttonSig"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/url"
android:layout_toRightOf="@+id/buttonAnt"
android:enabled="false"
android:onClick="siguiente"
android:text="@string/siguiente" />
<Button
android:id="@+id/buttonDetener"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/url"
android:layout_toRightOf="@+id/buttonSig"
android:enabled="false"
android:onClick="detener"
android:text="@string/detener" />
</RelativeLayout>
<WebView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/webkit"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
</LinearLayout>
|
Los métodos asociados a los botones no tiene ninguna complicación,
simplemente invocan métodos de WebView. En el caso del botón “Ir” he incluído
el código para ocultar el teclado.
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
|
package com.danielme.blog.android.webviewdemo;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
public class WebViewdemoActivity extends Activity
{
private WebView browser;
private ProgressBar
progressBar;
private EditText
url;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
browser =
(WebView)findViewById(R.id.webkit);
//habilitamos
javascript y el zoom
browser.getSettings().setJavaScriptEnabled(true);
browser.getSettings().setBuiltInZoomControls(true);
//habilitamos
los plugins (flash)
browser.getSettings().setPluginsEnabled(true);
browser.setWebViewClient(new WebViewClient()
{
//
evita que los enlaces se abran fuera nuestra app en el navegador de android
@Override
public boolean shouldOverrideUrlLoading(WebView
view, String url)
{
return false;
}
});
url = (EditText)
findViewById(R.id.url);
progressBar =
(ProgressBar) findViewById(R.id.progressbar);
browser.setWebChromeClient(new WebChromeClient()
{
@Override
public void onProgressChanged(WebView view, int progress)
{
progressBar.setProgress(0);
progressBar.setVisibility(View.VISIBLE);
WebViewdemoActivity.this.setProgress(progress
* 1000);
progressBar.incrementProgressBy(progress);
if(progress
== 100)
{
progressBar.setVisibility(View.GONE);
}
}
//la
implementación de este método permite para mostrar el title de la página en
la barra de título de la aplicación
@Override
public void onReceivedTitle(WebView view, String title)
{
WebViewdemoActivity.this.setTitle("WebView
demo: " + WebViewdemoActivity.this.browser.getTitle());
}
});
}
//
//////////BOTONES DE NAVEGACIÓN /////////
public void ir(View view)
{
//
oculta el teclado al pulsar el botón
InputMethodManager
inputMethodManager = (InputMethodManager)
getSystemService(INPUT_METHOD_SERVICE);
inputMethodManager.hideSoftInputFromWindow(url.getWindowToken(),
0);
//
he observado que si se pulsa "Ir" sin modificarse la url no se
//
ejecuta el método onPageStarted, así que nos aseguramos
//
que siempre que se cargue una url, aunque sea la que se está
//
mostrando, se active el botón "detener"
((Button)
WebViewdemoActivity.this.findViewById(R.id.buttonDetener)).setEnabled(true);
browser.loadUrl(url.getText().toString());
}
public void anterior(View view)
{
browser.goBack();
}
public void siguiente(View view)
{
browser.goForward();
}
public void detener(View view)
{
browser.stopLoading();
}
}
|
Lo más interesante de la barra es cómo controlar la carga de la página
para activar/desactivar los botones, y para ello implementaremos dos nuevos
métodos en nuestro WebViewClient
·
onPageStarted: Se ejecuta al iniciarse la
carga de una página, una vez por cada frame(el control de este último caso no
se contempla en este tutorial) .Es aquí donde se habilitará siempre el botón
“Detener”.Asimismo, en el cuadro de texto mostraremos la url que se está
cargando ya que es posible que se trate de un link que el usuario haya pulsado.
44
45
46
47
48
49
50
51
52
|
// gestión de los botones de
navegación (inicio de carga de una página)
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon)
{
//
mostramos la url de los enlaces seleccionados
WebViewdemoActivity.this.url.setText(url);
((Button)
WebViewdemoActivity.this.findViewById(R.id.buttonDetener)).setEnabled(true);
}
|
·
onPageFinished: Se invocará cuando haya
terminado la carga de la página, momento en el que hay que deshabilitar el
botón Detener al carecer de sentido. También se gestionarán
aquí los botones Anterior ySiguiente.
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
|
// gestión de los botones de
navegación (final de carga de una
//
página)
@Override
public void onPageFinished(WebView view, String url)
{
((Button)
WebViewdemoActivity.this.findViewById(R.id.buttonDetener)).setEnabled(false);
Button
botonAnterior = (Button)
WebViewdemoActivity.this.findViewById(R.id.buttonAnt);
if (view.canGoBack())
{
botonAnterior.setEnabled(true);
}
else
{
botonAnterior.setEnabled(false);
}
Button
botonSiguiente = (Button) WebViewdemoActivity.this.findViewById(R.id.buttonSig);
if (view.canGoForward())
{
botonSiguiente.setEnabled(true);
}
else
{
botonSiguiente.setEnabled(false);
}
}
|
¿Y el botón Back?
En nuestra demo al pulsar el botón “Back” del dispositivo finalizaremos
la Activity, mientras que en el navegador por defecto de Android se vuelve a la
página anterior y la Activity no se finaliza hasta que no pulsemos Back desde
la primera página mostrada. Este comportamiento puede ser implementado
fácilmente, aunque en el caso de que incrustemos el navegador en una app
probablemente resulte más conveniente mostrar una barra de navegación y no
modificar el comportamiento por defecto del botón Back.
//simula el comportamiento del
botón Back en el navegador de Android
@Override
public void onBackPressed()
{
if (browser.canGoBack())
{
browser.goBack();
}
else
{
super.onBackPressed();
}
}
|
Gestión de errores
WebView mostrará en pantalla los errores que se produzcan, pero podemos
recuperalos para actuar en consecuencia implementando el método onReceivedError de WebViewClient.
//gestión de errores
@Override
public void onReceivedError(WebView view, int errorCode,
String description, String failingUrl)
{
AlertDialog.Builder
builder = new AlertDialog.Builder(WebViewdemoActivity.this);
builder.setMessage(description).setPositiveButton("Aceptar",
null).setTitle("onReceivedError");
builder.show();
}
|
Descargas
Otra interesante funcionalidad proporcionada por WebView es el
listener DownloadListener que nos permitirá saber
cuándo WebView considera una url solicitada como descarga. A continuación se
expone una posible implementación de ejemplo que, con la aprobación del
usuario, descarga el fichero en la tarjeta SD por lo que hay darle permisos de
escritura a la aplicación. Por simplicidad los textos se han incluído en el
código, en una aplicación real deberían estar en el strings.xml para su abstracción
y una posible internacionalización. Para mayor información sobre la obtención
de recursos web, consultar el tip #8.Para el uso del espacio de
almacenamiento externo, ver el tip #5.
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
|
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
|
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
browser = (WebView)
findViewById(R.id.webkit);
faviconImageView =
(ImageView) findViewById(R.id.favicon);
WebIconDatabase.getInstance().open(getDir("icons",
MODE_PRIVATE).getPath());
// habilitamos
javascript y el zoom
browser.getSettings().setJavaScriptEnabled(true);
browser.getSettings().setBuiltInZoomControls(true);
//habilitamos los
plugins (flash)
browser.getSettings().setPluginsEnabled(true);
//implementa la acción a
realizar cuando WebView considere que el enlace solicitado es una descarga
browser.setDownloadListener(new DownloadListener()
{
public void onDownloadStart(final String
url, String userAgent, String contentDisposition, String mimetype, long contentLength)
{
AlertDialog.Builder
builder = new AlertDialog.Builder(WebViewdemoActivity.this);
builder.setTitle("Descarga");
builder.setMessage("¿Desea
guardar el fichero en su tarjeta SD?");
builder.setCancelable(false).setPositiveButton("Aceptar",
new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int id)
{
descargar(url);
}
}).setNegativeButton("Cancelar",
new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int id)
{
dialog.cancel();
}
});
builder.create().show();
}
private void descargar(final String
url)
{
String
resultado ="";
URL
urlObject = null;
InputStream
inputStream = null;
HttpURLConnection
urlConnection = null;
try
{
urlObject
= new URL(url);
urlConnection
= (HttpURLConnection) urlObject.openConnection();
inputStream
= urlConnection.getInputStream();
//se crea el fichero en el que
se almacenará
String
fileName = Environment.getExternalStorageDirectory().getAbsolutePath() +
"/webviewdemo";
File
directorio = new File(fileName);
File
file = new File(directorio, url.substring(url.lastIndexOf("/")));
//asegura que el directorio
exista
directorio.mkdirs();
FileOutputStream
fileOutputStream = new FileOutputStream(file);
ByteArrayOutputStream
byteArrayOutputStream = new ByteArrayOutputStream();
byte[]
buffer = new byte[1024];
int len = 0;
while (inputStream.available() > 0 &&
(len = inputStream.read(buffer)) != -1)
{
byteArrayOutputStream.write(buffer,
0, len);
}
fileOutputStream.write(byteArrayOutputStream.toByteArray());
fileOutputStream.flush();
resultado
= "guardado en : " + file.getAbsolutePath();
}
catch (Exception ex)
{
resultado
= ex.getClass().getSimpleName() + " " +
ex.getMessage();
}
finally
{
if (inputStream != null)
{
try
{
inputStream.close();
}
catch (IOException e)
{
}
}
if (urlConnection != null)
{
urlConnection.disconnect();
}
}
AlertDialog.Builder
builder = new AlertDialog.Builder(WebViewdemoActivity.this);
builder.setMessage(resultado).setPositiveButton("Aceptar",
null).setTitle("Descarga");
builder.show();
}
});
...
|
Sin embargo, esta aproximación al tratamiento de las descargas no es
correcta ya que a partir de HoneyComb no se pueden realizar llamadas a url
dentro del hilo principal de la aplicación; de lo contrario obtenemos esta bonita
excepción (aunque la aplicación no se cierra):
El motivo está bien fundamentado: estamos realizando operaciones que
pueden tomar bastante tiempo y no podemos dejar bloqueada la interfaz de
usuario. Por ello es necesario realizar el proceso de descarga en segundo plano
en otro hilo, usando por ejemplo Thread/handler o AsyncTask, estrategias
comentadas en la sección Tips Android (#2 y #3 respectivamente). Usando AsyncTask
para la descarga, el código quedará tal que así (también se validará la
disponibilidad de almacenamiento externo):
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
|
class DownloadAsyncTask extends AsyncTask<String,
Void, String>
{
@Override
protected String doInBackground(String... arg0)
{
String
result = null;
String
url = arg0[0];
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
{
URL
urlObject = null;
InputStream
inputStream = null;
HttpURLConnection
urlConnection = null;
try
{
urlObject
= new URL(url);
urlConnection
= (HttpURLConnection) urlObject.openConnection();
inputStream
= urlConnection.getInputStream();
String
fileName = Environment.getExternalStorageDirectory().getAbsolutePath() +
"/webviewdemo";
File
directory = new File(fileName);
File
file = new File(directory, url.substring(url.lastIndexOf("/")));
directory.mkdirs();
FileOutputStream
fileOutputStream = new FileOutputStream(file);
ByteArrayOutputStream
byteArrayOutputStream = new ByteArrayOutputStream();
byte[]
buffer = new byte[1024];
int len = 0;
while (inputStream.available() > 0
&& (len = inputStream.read(buffer)) != -1)
{
byteArrayOutputStream.write(buffer,
0, len);
}
fileOutputStream.write(byteArrayOutputStream.toByteArray());
fileOutputStream.flush();
result
= "guardado en : " +
file.getAbsolutePath();
}
catch (Exception ex)
{
result
= ex.getClass().getSimpleName() + " " +
ex.getMessage();
}
finally
{
if (inputStream != null)
{
try
{
inputStream.close();
}
catch (IOException ex)
{
result
= ex.getClass().getSimpleName() + " " +
ex.getMessage();
}
}
if (urlConnection != null)
{
urlConnection.disconnect();
}
}
}
else
{
result
= "Almacenamiento no disponible";
}
return result;
}
@Override
protected void onPostExecute(String result)
{
AlertDialog.Builder
builder = new AlertDialog.Builder(WebViewdemoActivity.this);
builder.setMessage(result).setPositiveButton("Aceptar",
null).setTitle("Descarga");
builder.show();
}
}
|
Ahora, en lugar de llamar al método descargar(url) lanzamos la
AsyncTask:
71
72
73
74
75
76
77
|
builder.setCancelable(false).setPositiveButton("Aceptar",
new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int id)
{
(new DownloadAsyncTask()).execute(url);
}
...
|
En Gingerbread se introdujo el servicio DownloadManager diseñado para realizar
grandes descargas en segundo plano. Hay un pequeño ejemplo de uso en este tip.
Favicon
En principio utilizar el favicon de
las páginas visitadas por WebView parece sencillo ya que el métodoonPageStarted recibe un Bitmap llamado favicon. Sin embargo, este
parámetro siempre es nulo ya que según la documentación la imagen sólo se
recibe si ya existe previamente en una base de datos empleada por WebView
(*): WebIconDatabase.
(*) Según la documentación oficial a partir de Android
4.3 ya no es necesaria utilizar esta clase por lo que ha sido “deprecada”. Sin
embargo, he probado a no usarla y no se reciben los íconos.
Siguiendo las instrucciones del segundo post de este hilo podemos incluir el procesamiento del
favicon de forma muy simple y mostrarlo junto a la url:
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/favicon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_alignBottom="@+id/url"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:src="@drawable/favicon_default" />
<EditText
android:id="@+id/url"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_toLeftOf="@+id/buttonIr"
android:layout_toRightOf="@+id/favicon"
android:ems="10"
android:inputType="textUri" >
|
El icono por defecto que he usado pertenece a esta versión de la colección oficial de gráficos de Google,
se encuentra en el el tema “Holo Dark” y se llama originalmente
“7-location-web-site.png”. Está disponible en varias densidades.
Ahora para mostrar el favicon adecuado:
1. Añadir el widget a
la Activity
41
|
private ImageView faviconImageView;
|
2. Iniciar
WebIconDatabase en el onCreate de la activity:
50
51
|
faviconImageView = (ImageView) findViewById(R.id.favicon);
WebIconDatabase.getInstance().open(getDir("icons",
MODE_PRIVATE).getPath());
|
3. Recibir el favicon
en cuanto sea obtenido por WebView gracias al método onReceivedIcon.Si no se inicia WebIconDatabase
este método nunca se invoca.
241
242
243
244
245
|
@Override
public void onReceivedIcon(WebView view, Bitmap icon)
{
faviconImageView.setImageBitmap(icon);
}
|
El resultado final con el favicon junto a la dirección:
Conectividad
Siempre que en una app necesitemos conectividad, es una buena práctica
comprobar que la tenemos disponible antes de solicitar cualquier operación y
esperar a recibir un timeout o error similar. Para ello hay que hacer uso deConnectivityManager y NetworkInfo , yo siempre utilizo el
siguiente snippet:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
private boolean checkConnectivity()
{
boolean enabled = true;
ConnectivityManager
connectivityManager = (ConnectivityManager)
this.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo
info = connectivityManager.getActiveNetworkInfo();
if ((info == null ||
!info.isConnected() || !info.isAvailable()))
{
enabled
= false;
Builder
builder = new Builder(this);
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setMessage(getString(R.string.noconnection));
builder.setCancelable(false);
builder.setNeutralButton(R.string.ok,
null);
builder.setTitle(getString(R.string.error));
builder.create().show();
}
return enabled;
}
|
Nota: El código anterior es muy básico, y si fuera necesario se puede
obtener más información sobre la conexión activa a través del método getType(). Si necesitamos realizar un uso
intensivo de la red, puede ser por ejemplo conveniente avisar al usuario si
utiliza una conexión de datos móvil en lugar de wifi.
Obviamente necesitamos los nuevos textos en strings.xml:
8
9
10
|
<string name="noconnection">La conexión a
internet no se encuentra disponible en estos momentos</string>
<string name="ok">Aceptar</string>
<string name="error">Error</string>
|
También necesitamos añadir al manifest el permiso para
acceder al estado de la red, con el de acceso a Internet que ya teníamos no es
suficiente:
9
10
11
|
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
Para cubrir tanto las peticiones realizadas el escribir manualmente la
url como los links pulsados, añadimos la validación a los métodos onPageStarted y
los de los botones de navegación:
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
|
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon)
{
if (checkConnectivity())
{
//
mostramos la url de los enlaces seleccionados
WebViewdemoActivity.this.url.setText(url);
((Button)
WebViewdemoActivity.this.findViewById(R.id.buttonDetener)).setEnabled(true);
}
else
{
((Button)
WebViewdemoActivity.this.findViewById(R.id.buttonDetener)).setEnabled(false);
}
}
|
|
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
|
public void ir(View view)
{
//
oculta el teclado al pulsar el botón
InputMethodManager
inputMethodManager = (InputMethodManager)
getSystemService(INPUT_METHOD_SERVICE);
inputMethodManager.hideSoftInputFromWindow(url.getWindowToken(),
0);
if(checkConnectivity())
{
//
he observado que si se pulsa "Ir" sin modificarse la url no se
//
ejecuta el método onPageStarted, así que nos aseguramos
//
que siempre que se cargue una url, aunque sea la que se está
//
mostrando, se active el botón "detener"
((Button)
WebViewdemoActivity.this.findViewById(R.id.buttonDetener)).setEnabled(true);
browser.loadUrl(url.getText().toString());
}
}
public void anterior(View view)
{
if(checkConnectivity())
{
browser.goBack();
}
}
public void siguiente(View view)
{
if(checkConnectivity())
{
browser.goForward();
}
}
|
Mostrar el teclado virtual en los
input
En algunas pruebas he encontrado un error muy molesto:al hacer click en
un input text de una web no se mostraba el teclado y, por lo tanto, no podía
hacer uso de él. Parece ser que esto se debe al siguiente bug, la “solución”, simplemente copio y pego,
es añadir al onCreate el siguiente código:
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
|
browser.setOnTouchListener(new View.OnTouchListener()
{
@Override
public boolean onTouch(View
v, MotionEvent event)
{
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_UP:
if (!v.hasFocus())
{
v.requestFocus();
}
break;
}
return false;
}
});
|
Gestionar las rotaciones
Uno de mis últimos tutoriales estuvo dedicado a la gestión de rotaciones en Android. En el
navegador que estamos implementando, si rotamos la pantalla la Activity se
reinicia tal y como se analizó en ese artículo y volvemos a la pantalla en
blanco de inicio, perdiendo lo que tuviéramos en pantalla. Para evitar esto,
vamos a ir a la solución sencilla ya que en este caso no tenemos un layout para
el modo landscape, y simplemente deshabilitamos la gestión de rotaciones por
defecto de Android configurando nuestra única Activity de la siguiente forma:
<activity
android:name=".WebViewdemoActivity"
android:label="@string/app_name"
android:configChanges="orientation|keyboardHidden">
|
Para más detalles, remito al lector al mencionado artículo.
Ejemplo más completo en GitHub
Aunque la demo cumple con lo que quería mostrar cuando me planteé la
elaboración de este tutorial (que por otra parte es consecuencia de haber
tenido que utilizar WebView en uno de mis proyectos), le he dedicado algo más
de tiempo para implementar algunas mejoras:
·
Página de inicio en html
·
Información en la barra de progreso
·
Internacionalización
·
Historial
Esta nueva demo está publicada publicada en GitHub y en la sección de demos del blog. Para más
información sobre cómo utilizar GitHub, consultar este artículo.
En el fondo la construcción del navegador no tiene mucho sentido como
fin, pero sí como medio y divertimento para seguir aprendiendo a desarrollar en
Android. Espero que mi trabajo os haya sido de ayuda ;)
Lo nuevo en Android 4.4 Kit Kat
Una de las grandes novedades de la última versión de Android en el
momento de escribir estas líneas es el “nuevo” WebView ya que a partir de esta
versión se basa directamente en el motor de Chromium,
concretamente en su versión 30. Chromium es el navegador Open Source en el que
se basa Google Chrome y según Google con este cambio podemos esperar una
notable mejora del rendimiento en la ejecución de javascript gracias a su motor
y de CSS gracias a la aceleración hardware. También se mejora el soporte de
HTML 5. Este cambio debería repercutir en una importante mejora de las
aplicaciones nativas basadas en tecnologías Web como las desarrolladas con
PhoneGap. Por otra parte, la API del propio WebView que hemos visto en este
artículo no ha sufrido cambios importantes.
Aquí se puede encontrar una pequeña guía
de migración para las aplicaciones basadas en WebView.
esto ya no funciona! sirven algunas cosas como el progressbar los botones.. pero el sistema de descarga no funciona!
ResponderEliminar