Android WebView: manejo de los cambios de orientación

El problema es el rendimiento después de la rotación. WebView tiene que volver a cargar la página, lo que puede ser un poco tedioso.

¿Cuál es la mejor manera de manejar un cambio de orientación sin volver a cargar la página desde la fuente cada vez?

Si no desea que WebView vuelva a cargarse en los cambios de orientación, simplemente anule onConfigurationChanged en su clase de actividad:

@Override public void onConfigurationChanged(Configuration newConfig){ super.onConfigurationChanged(newConfig); } 

Y configure el atributo android: configChanges en el manifiesto:

  

para más información ver:
http://developer.android.com/guide/topics/resources/runtime-changes.html#HandlingTheChange

https://developer.android.com/reference/android/app/Activity.html#ConfigurationChanges

Editar: Este método ya no funciona como se indica en los documentos


Respuesta original:

Esto se puede manejar sobrescribiendo onSaveInstanceState(Bundle outState) en su actividad y llamando a saveState desde la vista web:

  protected void onSaveInstanceState(Bundle outState) { webView.saveState(outState); } 

Luego, recupere esto en su onCreate después de que la vista web se haya vuelto a inflar por supuesto:

 public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.blah); if (savedInstanceState != null) ((WebView)findViewById(R.id.webview)).restreState(savedInstanceState); } 

La mejor respuesta a esto es seguir la documentación de Android que se encuentra aquí. Básicamente, esto evitará que Webview se vuelva a cargar:

  

En la actividad:

 @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); // Checks the orientation of the screen if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show(); } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show(); } } 

Intenté usar onRetainNonConfigurationInstance (devolver el WebView ), luego recuperarlo con getLastNonConfigurationInstance durante onCreate y reasignarlo.

No parece funcionar todavía. ¡No puedo evitar pensar que estoy muy cerca! Hasta el momento, me acabo de dar un WebView en blanco / fondo blanco. Publicando aquí con la esperanza de que alguien pueda ayudar a que este pase la línea de meta.

Tal vez no debería pasar el WebView . Tal vez un objeto desde el WebView ?

El otro método que probé, no mi favorito, es establecer esto en la actividad:

  android:configChanges="keyboardHidden|orientation" 

… y luego no haces casi nada aquí:

 @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); // We do nothing here. We're only handling this to keep orientation // or keyboard hiding from causing the WebView activity to restart. } 

ESO funciona, pero podría no ser considerado una mejor práctica .

Mientras tanto, también tengo un solo ImageView que quiero actualizar automágicamente según la rotación. Esto resulta ser muy fácil. Debajo de mi carpeta de res , tengo drawable-land y drawable-port para guardar variaciones de paisaje / retrato, luego uso R.drawable.myimagename para la fuente de ImageView y Android “hace lo correcto” – ¡yay!

… excepto cuando mira los cambios de configuración, entonces no es así. 🙁

Entonces estoy en desacuerdo Utilice onRetainNonConfigurationInstance y la rotación de ImageView funciona, pero la persistencia de WebView no … o use onConfigurationChanged y WebView se mantiene estable, pero ImageView no se actualiza. ¿Qué hacer?

Una última nota: en mi caso, forzar la orientación no es un compromiso aceptable. Realmente queremos apoyar con gracia la rotación. ¡Algo así como la aplicación del navegador Android! 😉

Un compromiso es evitar la rotación. Agregue esto para arreglar la actividad solo para orientación vertical.

 android:screenOrientation="portrait" 

Puede intentar usar onSaveInstanceState() y onRestoreInstanceState() en su Activity para llamar a saveState(...) y restreState(...) en su instancia de WebView.

Aprecio que esto sea un poco tarde, sin embargo, esta es la respuesta que utilicé al desarrollar mi solución:

AndroidManifest.xml

    

WebClient.java

 public class WebClient extends Activity { protected FrameLayout webViewPlaceholder; protected WebView webView; private String WEBCLIENT_URL; private String WEBCLIENT_TITLE; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_web_client); initUI(); } @SuppressLint("SetJavaScriptEnabled") protected void initUI(){ // Retrieve UI elements webViewPlaceholder = ((FrameLayout)findViewById(R.id.webViewPlaceholder)); // Initialize the WebView if necessary if (webView == null) { // Create the webview webView = new WebView(this); webView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); webView.getSettings().setSupportZoom(true); webView.getSettings().setBuiltInZoomControls(true); webView.setScrollBarStyle(WebView.SCROLLBARS_OUTSIDE_OVERLAY); webView.setScrollbarFadingEnabled(true); webView.getSettings().setJavaScriptEnabled(true); webView.getSettings().setPluginState(android.webkit.WebSettings.PluginState.ON); webView.getSettings().setLoadsImagesAutomatically(true); // Load the URLs inside the WebView, not in the external web browser webView.setWebViewClient(new SetWebClient()); webView.setWebChromeClient(new WebChromeClient()); // Load a page webView.loadUrl(WEBCLIENT_URL); } // Attach the WebView to its placeholder webViewPlaceholder.addView(webView); } private class SetWebClient extends WebViewClient { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); return true; } } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.web_client, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.action_settings) { return true; }else if(id == android.R.id.home){ finish(); return true; } return super.onOptionsItemSelected(item); } @Override public void onBackPressed() { if (webView.canGoBack()) { webView.goBack(); return; } // Otherwise defer to system default behavior. super.onBackPressed(); } @Override public void onConfigurationChanged(Configuration newConfig){ if (webView != null){ // Remove the WebView from the old placeholder webViewPlaceholder.removeView(webView); } super.onConfigurationChanged(newConfig); // Load the layout resource for the new configuration setContentView(R.layout.activity_web_client); // Reinitialize the UI initUI(); } @Override protected void onSaveInstanceState(Bundle outState){ super.onSaveInstanceState(outState); // Save the state of the WebView webView.saveState(outState); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState){ super.onRestoreInstanceState(savedInstanceState); // Restore the state of the WebView webView.restreState(savedInstanceState); } } 

Adaptado de aquí

Espero que esto ayude 🙂

Es 2015, y muchas personas están buscando una solución que todavía funcione en los teléfonos Jellybean, KK y Lollipop. Después de muchas dificultades, encontré una forma de preservar la vista web intacta después de cambiar de orientación. Mi estrategia es básicamente almacenar la vista web en una variable estática separada en otra clase. Luego, si ocurre la rotación, desvinculo la vista web de la actividad, espero a que la orientación termine y vuelvo a adjuntar la vista web a la actividad. Por ejemplo … primero pon esto en tu MANIFIESTO (tecladoHidden y el teclado son opcionales):

    

En una CLASE DE APLICACIÓN SEPARADA, escriba:

  public class app extends Application { public static WebView webview; public static FrameLayout webviewPlaceholder;//will hold the webview @Override public void onCreate() { super.onCreate(); //dont forget to put this on the manifest in order for this onCreate method to fire when the app starts: android:name="com.myapp.abc.app" setFirstLaunch("true"); } public static String isFirstLaunch(Context appContext, String s) { try { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(appContext); return prefs.getString("booting", "false"); }catch (Exception e) { return "false"; } } public static void setFirstLaunch(Context aContext,String s) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(aContext); SharedPreferences.Editor editor = prefs.edit(); editor.putString("booting", s); editor.commit(); } } 

En la ACTIVIDAD poner:

 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if(app.isFirstLaunch.equals("true"))) { app.setFirstLaunch("false"); app.webview = new WebView(thisActivity); initWebUI("www.mypage.url"); } } @Override public void onRestoreInstanceState(Bundle savedInstanceState) { restreWebview(); } public void restreWebview(){ app.webviewPlaceholder = (FrameLayout)thisActivity.findViewById(R.id.webviewplaceholder); if(app.webviewPlaceholder.getParent()!=null&&((ViewGroup)app.webview.getParent())!=null) { ((ViewGroup) app.webview.getParent()).removeView(app.webview); } RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.FILL_PARENT, RelativeLayout.LayoutParams.FILL_PARENT); app.webview.setLayoutParams(params); app.webviewPlaceholder.addView(app.webview); app.needToRestoreWebview=false; } protected static void initWebUI(String url){ if(app.webviewPlaceholder==null); app.webviewPlaceholder = (FrameLayout)thisActivity.findViewById(R.id.webviewplaceholder); app.webview.getSettings().setJavaScriptEnabled(true); app.webview.getSettings().setJavaScriptCanOpenWindowsAutomatically(true); app.webview.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); app.webview.getSettings().setSupportZoom(false); app.webview.getSettings().setBuiltInZoomControls(true); app.webview.setScrollBarStyle(WebView.SCROLLBARS_OUTSIDE_OVERLAY); app.webview.setScrollbarFadingEnabled(true); app.webview.getSettings().setLoadsImagesAutomatically(true); app.webview.loadUrl(url); app.webview.setWebViewClient(new WebViewClient()); if((app.webview.getParent()!=null)){//&&(app.getBooting(thisActivity).equals("true"))) { ((ViewGroup) app.webview.getParent()).removeView(app.webview); } app.webviewPlaceholder.addView(app.webview); } 

Finalmente, el XML simple:

    

Hay varias cosas que podrían mejorarse en mi solución, pero ya pasé mucho tiempo, por ejemplo: una forma más corta de validar si la Actividad se ha lanzado por primera vez en lugar de usar el almacenamiento de Preferencias Compartidas. Este enfoque le preserva la vista web intacta (afaik), sus cuadros de texto, tags, UI, variables de JavaScript y estados de navegación que no se reflejan en la url.

Actualización: la estrategia actual es mover la instancia de WebView a la clase de Aplicación en lugar de un fragmento retenido cuando se separa y volver a adjuntar en la hoja de vida como lo hace Josh. Para evitar que la Aplicación se cierre, debe usar el servicio de primer plano, si desea conservar el estado cuando el usuario cambie de aplicación.

Si está utilizando fragmentos, puede usar la instancia de retención de WebView. La vista web se conservará como miembro de instancia de la clase. Sin embargo, debe adjuntar la vista web en OnCreateView y desacoplar antes de OnDestroyView para evitar que se destruya con el contenedor principal.

 class MyFragment extends Fragment{ public MyFragment(){ setRetainInstance(true); } private WebView webView; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = .... LinearLayout ll = (LinearLayout)v.findViewById(...); if (webView == null) { webView = new WebView(getActivity().getApplicationContext()); } ll.removeAllViews(); ll.addView(webView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); return v; } @Override public void onDestroyView() { if (getRetainInstance() && webView.getParent() instanceof ViewGroup) { ((ViewGroup) webView.getParent()).removeView(webView); } super.onDestroyView(); } } 

Créditos de PS van a la respuesta de Kcoppock

En cuanto a ‘SaveState ()’ ya no funciona según la documentación oficial :

Tenga en cuenta que este método ya no almacena los datos de visualización para este WebView. El comportamiento anterior podría potencialmente perder archivos si nunca se llamó a restreState (Bundle).

 @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); } @Override protected void onRestoreInstanceState(Bundle state) { super.onRestoreInstanceState(state); } 

Estos métodos pueden anularse en cualquier actividad, básicamente te permite guardar y restaurar valores cada vez que se crea / destruye una actividad, cuando la orientación de la pantalla cambia, la actividad se destruye y se recrea en el fondo, por lo que podrías utilizar estos métodos para almacenar / restaurar temporalmente estados durante el cambio.

Debería echar un vistazo más profundo a los dos métodos siguientes y ver si se ajusta a su solución.

http://developer.android.com/reference/android/app/Activity.html

La mejor solución que he encontrado para hacer esto sin tener que filtrar la referencia de Activity anterior y sin establecer los configChanges … es usar un MutableContextWrapper .

Lo he implementado aquí: https://github.com/slightfoot/android-web-wrapper/blob/48cb3c48c457d889fc16b4e3eba1c9e925f42cfb/WebWrapper/src/com/example/webwrapper/BrowserActivity.java

Lo único que debe hacer es agregar este código a su archivo de manifiesto:

  

Esto es lo único que funcionó para mí (incluso utilicé el estado de guardar la instancia en onCreateView pero no fue tan confiable).

 public class WebViewFragment extends Fragment { private enum WebViewStateHolder { INSTANCE; private Bundle bundle; public void saveWebViewState(WebView webView) { bundle = new Bundle(); webView.saveState(bundle); } public Bundle getBundle() { return bundle; } } @Override public void onPause() { WebViewStateHolder.INSTANCE.saveWebViewState(myWebView); super.onPause(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_main, container, false); ButterKnife.inject(this, rootView); if(WebViewStateHolder.INSTANCE.getBundle() == null) { StringBuilder stringBuilder = new StringBuilder(); BufferedReader br = null; try { br = new BufferedReader(new InputStreamReader(getActivity().getAssets().open("start.html"))); String line = null; while((line = br.readLine()) != null) { stringBuilder.append(line); } } catch(IOException e) { Log.d(getClass().getName(), "Failed reading HTML.", e); } finally { if(br != null) { try { br.close(); } catch(IOException e) { Log.d(getClass().getName(), "Kappa", e); } } } myWebView .loadDataWithBaseURL("file:///android_asset/", stringBuilder.toString(), "text/html", "utf-8", null); } else { myWebView.restreState(WebViewStateHolder.INSTANCE.getBundle()); } return rootView; } } 

Hice un titular de Singleton para el estado de WebView. El estado se conserva mientras exista el proceso de la aplicación.

EDITAR: loadDataWithBaseURL no era necesario, funcionó igual de bien con solo

  //in onCreate() for Activity, or in onCreateView() for Fragment if(WebViewStateHolder.INSTANCE.getBundle() == null) { webView.loadUrl("file:///android_asset/html/merged.html"); } else { webView.restreState(WebViewStateHolder.INSTANCE.getBundle()); } 

Aunque leo esto, no necesariamente funciona bien con las cookies.

La mejor forma de manejar cambios de orientación y evitar la recarga de WebView en Girar.

 @Override public void onConfigurationChanged(Configuration newConfig){ super.onConfigurationChanged(newConfig); } 

Con eso en mente, para evitar que se llame a onCreate () cada vez que cambie de orientación, debería agregar android:configChanges="orientation|screenSize" to the AndroidManifest.

o solo ..

 android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize"` 

Simplemente escriba las siguientes líneas de código en su archivo Manifiesto, nada más. Realmente funciona:

  

Esta página soluciona mi problema, pero tengo que hacer un pequeño cambio en la inicial:

  protected void onSaveInstanceState(Bundle outState) { webView.saveState(outState); } 

Esta parte tiene un pequeño problema para mí esto. En la segunda orientación, cambie la aplicación terminada con un puntero nulo

usando esto funcionó para mí:

  @Override protected void onSaveInstanceState(Bundle outState ){ ((WebView) findViewById(R.id.webview)).saveState(outState); } 

Deberías probar esto:

  1. Cree un servicio, dentro de ese servicio, cree su WebView.
  2. Comience el servicio de su actividad y comprométalo.
  3. En el método onServiceConnected , obtenga WebView y llame al método setContentView para representar su WebView.

Lo probé y funciona, pero no con otras WebView como XWalkView o GeckoView.

 @Override protected void onSaveInstanceState(Bundle outState ) { super.onSaveInstanceState(outState); webView.saveState(outState); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); webView.restreState(savedInstanceState); } 

Me gusta esta solución http://www.devahead.com/blog/2012/01/preserving-the-state-of-an-android-webview-on-screen-orientation-change/

Según esto, reutilizamos la misma instancia de WebView. Permite guardar el historial de navegación y la posición de desplazamiento en el cambio de configuración.