OnKeyDown inside WebView fragment - android

I made a Fragment, containing a WebView and I wanted to define an onKeyDown() to get back from one web page to the previous one. I did it, but the weirdest part for me was to share a WebView variable from my Fragment class to my Activity class because I can't define onKeyDown() in the Fragment. So I just defined a get method and made it static. But I wonder if it was a real mistake and my app can seriously crash in some circumstances.
My Fragment code:
public class BrowserFragment extends Fragment {
private static WebView webView;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState){
View v = inflater.inflate(R.layout.fragment_activity, parent, false);
getActivity().setTitle(R.string.title_rus);
webView = (WebView) v.findViewById(R.id.webView);
webView.setWebViewClient(new SwingBrowserClient());
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
Uri data = Uri.parse("http://www.swinginmoscow.ru/m/");
webView.loadUrl(data.toString());
return v;
}
public static WebView getWebView() {
return webView;
}
}
And my Activity code:
public class MainBrowserActivity extends SingleFragmentActivity {
#Override
protected Fragment createFragment() {
return new BrowserFragment();
}
#Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if ((keyCode == KeyEvent.KEYCODE_BACK) && BrowserFragment.getWebView().canGoBack()) {
BrowserFragment.getWebView().goBack();
return true;
}
return super.onKeyDown(keyCode, event);
}
}

It might work, but it is not a good idea. It could very well cause a crash if you don't handle the Fragment correctly or are somewhere in your code a little careless regarding its lifecycle. But there is an easy way around this. Instead of using a static method, save the instance and call methods on the instance itself. This way you can check if the instance is null and if not the Fragment can handle calls to goBack() or canGoBack() itself:
public class MainBrowserActivity extends SingleFragmentActivity {
BrowserFragment browserFragment = null;
#Override
protected Fragment createFragment() {
this.browserFragment = BrowserFragment.newInstance();
return this.browserFragment;
}
#Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && this.browserFragment != null && this.browserFragment.canGoBack()) {
this.browserFragment.goBack();
return true;
}
return super.onKeyDown(keyCode, event);
}
}
As you can see the BrowserFragment instance is saved and then methods like goBack() or canGoBack() are called on the BrowserFragment itself. Of course you have to implement these methods in the BrowserFragment but that should not be a problem:
public class BrowserFragment extends Fragment {
public static BrowserFragment newInstance() {
BrowserFragment fragment = new BrowserFragment();
return fragment;
}
private WebView webView;
public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState){
View v = inflater.inflate(R.layout.fragment_activity, parent, false);
getActivity().setTitle(R.string.title_rus);
webView = (WebView) v.findViewById(R.id.webView);
webView.setWebViewClient(new SwingBrowserClient());
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
Uri data = Uri.parse("http://www.swinginmoscow.ru/m/");
webView.loadUrl(data.toString());
return v;
}
public boolean canGoBack() {
return this.webView != null && this.webView.canGoBack();
}
public void goBack() {
if(this.webView != null) {
this.webView.goBack();
}
}
}
I made a few extra improvements to your code. First of all I added null checks to prevent any possible NullPointerExceptions and secondly it is recommend to always use a static factory method to create new instances of Fragments. That is what the static newInstance() method is that I added to the BrowserFragment. The advantage of that is that you can implement a method which takes care of setting up the BrowserFragment for you regardless of where you use it. You can add parameters to newInstance() method to pass some value to the BrowserFragment or to add some required listener etc but since you don't pass any values to the BrowserFragment the newInstance() method remains pretty empty. Nevertheless it is best practice to always use such factory methods even if they only call new BrowserFragment().
Generally this approach is much better. Especially from a architecture perspective because you don't directly interact with the WebView in the Activity. The WebView doesn't have anything to do with the Activity, it is part of the implementation of the BrowserFragment and as such the Activity should not know that there even is a WebView. How calls to goBack() or canGoBack() are implemented or what they exactly do is of no interest to the Activity. The Activity just tells the BrowserFragment "I want to go back" and the BrowserFragment does the work. This separates the responsibilities better and makes the code more readable, more clear and more maintainable.
EDIT:
Also I don't know of a SingleFragmentActivity but generally any Activity implements onBackPressed() method. You don't have to override onKeyDown() to catch a back key event. You can just do something like this:
#Override
public void onBackPressed() {
if (this.browserFragment != null && this.browserFragment.canGoBack()) {
this.browserFragment.goBack();
} else {
// The back key event only counts if we execute super.onBackPressed();
super.onBackPressed();
}
}
If you have any other questions please feel free to ask!

Related

Activity crashes when minimized and a dialog is opened

On my activity a show a Custom DialogFragement. But when I minimize the app while the dialog is opened i get this error
java.lang.RuntimeException: Parcelable encountered IOException writing serializable object (name = xxx.xxx.MyActivity$24)
at android.os.Parcel.writeSerializable(Parcel.java:1468)
at android.os.Parcel.writeValue(Parcel.java:1416)
at android.os.Parcel.writeArrayMapInternal(Parcel.java:686)
at android.os.BaseBundle.writeToParcelInner(BaseBundle.java:1330)
at android.os.Bundle.writeToParcel(Bundle.java:1079)
at android.os.Parcel.writeBundle(Parcel.java:711)
at android.support.v4.app.FragmentState.writeToParcel(Fragment.java:144)
at android.os.Parcel.writeTypedArray(Parcel.java:1254)
at ....
... 23 more
java.io.NotSerializableException: xxx.xxx.MyActivity
at java.io.ObjectOutputStream.writeNewObject(ObjectOutputStream.java:1344)
xxxxx
For the moment to fix this I have set this in the dialog fragment which seems to works but I don't like these kind of of dirty tricks
#Override
public void onPause() {
dismiss();
super.onPause();
}
Anyone can tell me what causes this exception in the first place ?
The onCreate of the custom DialogFragment:
public static MyCustomDialog newInstance(double lat, double lng, MyListener listener) {
MyCustomDialog dialogFragment = new MyCustomDialog();
Bundle bundle = new Bundle();
bundle.putDouble(ARG_LAT, lat);
bundle.putDouble(ARG_LNG, lng);
bundle.putSerializable(ARG_LISTENER, listener);
dialogFragment.setArguments(bundle);
return dialogFragment;
}
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.dialog_layout, null, false);
final double lat = getArguments().getDouble(ARG_LAT);
final double lng = getArguments().getDouble(ARG_LNG);
listener = (MyListener) getArguments().getSerializable(ARG_LISTENER);
TextView saveBtn = (TextView) view.findViewById(R.id.save_btn);
saveBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
/* some processing */
listener.onSave();
}
});
return view;
}
MyListener is an interface. I thought at first that this could have been the problem but even if I dont put in the bundle I get the same error, so it must be from something else.
Activity side inside a button on click:
MyCustomDialog myCustomDialog = MmyCustomDialog.newInstance(lat, lon, listener);
myCustomDialog.show(getSupportFragmentManager(), "tag");
I see this line in your code:
listener = (MyListener) getArguments().getSerializable(ARG_LISTENER);
If I guess correctly, MyListener is implemented either by an inner class of your Activity or by the Activity itself. Either way, MyListener holds a reference to the Activity. Please note that if your are creating an anonymous inner class, that is:
MyListener listener = new MyListener() { ... }
it still holds a reference to your Activity.
When the Fragment needs to be destroyed, the arguments Bundle is stored in the fragment state Parcel. Android tries to serialize MyListener, however Activity is not Serializable and it fails.
The usual pattern is to declare your listener interface in your Fragment:
public class MyFragment extends Fragment {
...
public interface Callbacks {
void doSomething();
}
...
Then an Activity that hosts the Fragment implements this interface:
public class MyActivity extends AppCompatActivity implements MyFragment.Callbacks {
...
#Override
public void doSomething() {
// do your stuff
}
...
In onAttach of your Fragment you cast the Fragment's host to your Callbacks and set the field.
#Override
public void onAttach(Context context) {
super.onAttach(context);
if (getHost() instanceof Callbacks) {
mCallbacks = (Callbacks) getHost();
} else {
throw new AssertionError("Host does not implement Callbacks!");
}
}
PS In the documentation you may see a version of onAttach method taking an Activity. The version above is deprecated from API 23. Until then, Fragments could be hosted only by Activities. Now, Fragments can be hosted by arbitrary objects and operate in any Context. That being said, you probably will host your Fragments inside Activities anyway, and your context and host will be the same.
try using this method
dismissAllowingStateLoss();
instead of
dismiss();

Generic handle OnBackPressed()

I have a BaseActivity() that have many activities and a BaseFragment() that have many fragments. Each activity contains 2-3 fragments and I need to make a generic method to handle each onBackPressed from all fragments (all - means all app screens) but this method should be in Base Fragment() (every fragment extends it). I supose that I'll need a kind of listener to tie OnBackPressed() from BaseActivity() to genericMethod() from BaseFragment()
Thanks in advice.
#Choletski:
onBackPressed()
It will be called when the activity has detected the user's press of the back key. The default implementation simply finishes the current activity, but you can override this to do whatever you want.while overriding the default back button action as it is not suggested to change the android default user experience.
Override the onBackPressed() method and take the action inside this function.
#Override
public void onBackPressed() {
// Write your code here
super.onBackPressed();
}
How to implement onBackPressed() in Android Fragments?
The simplest solution rest to be a bit "hard programmed" in my case, like I mentioned in my question I need a method from BaseFragment() to handle all back press actions from all screens that means all fragments that extends this BaseFragment().
#Sharp Edge solution may be accepted but why to handle it in each SimpleActivity() that extends BaseActivity() if I can just add a single method in BaseFragment() and all simple activities that extends BaseActivity() will don't care about that.
#escape-llc solution is confused and not the expected one... I can handle it easier using EventBus or Otto and send from onResume() from each fragment to SimpleActivity(). So I'll receive the actual open fragment and I'll now what action to do when onBackPressed() is executed...
So, like I said, my solution is to use just a simple generic method in BaseFragment():
public void doBackBtnPressedAction(View view) {
view.setFocusableInTouchMode(true);
view.requestFocus();
view.setOnKeyListener(new View.OnKeyListener() {
#Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
//logical part, in my case some server requests
return true;
}
}
return false;
}
});
}
This is how i handled it when i had a webview in fragment and wanted to handle onBackPressed for the webview?
public class Tab2 extends Fragment {
ProgressBar progress;
WebView x;
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View v =inflater.inflate(R.layout.tab_2,container,false);
x = (WebView)v.findViewById(R.id.webView);
x.setOnKeyListener(new OnKeyListener() {
#Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if(event.getAction() == KeyEvent.ACTION_DOWN)
{
WebView web = (WebView)v;
switch (keyCode)
{
case KeyEvent.KEYCODE_BACK:
if(web.canGoBack())
{
web.goBack();
return true;
}
break;
}
}
return false;
}
});
You have to make a custom Activity class for this.. and override its on onBackPressed() and add your logic in their. Then make sure wherever Fragments are used, you have to make the associated Activity sub class of this CustomActivity..
So whenever no matter on which Fragment user is, onBackPressed() of that Activity will be called and add super() to it.. so that it will call the base class's method and your code will run on each fragment.
example:
MyCustomActvity extends FragmentActivity{
#Override
public void onBackPressed(){
// your logic here
super.onBackPressed();
}
}
Now You know that Fragments must have at least 1 Base Activity, so just override that Activity's onBackPressed()
MyActivity extends MyCustomActivity{
// 3 fragments are called/replaced from this activity
// other code
#Override
public void onBackPressed(){
super.onBackPressed(); // it will invoke base class method and your code
}
}
Just extend MyCustomActivity for the ones which use Fragments.
Here is a great way to handle it in a general fashion. We use it now in all of our fragment-based apps.
First create an interface for fragments to implement. This represents whether they want to handle the back key at all. If not, don't implement the interface.
public interface IHandleBackPressed {
boolean handleBackPressed(Activity ax);
}
This is essentially a proxy for the activity's onBackPressed method.
Next, override the Activity.onBackPressed method with this boilerplate:
#Override
public void onBackPressed() {
final Fragment fx = getFragmentManager().findFragmentById(R.id.content);
if(fx != null) {
if(fx instanceof IHandleBackPressed) {
final IHandleBackPressed ihbp = (IHandleBackPressed)fx;
if(ihbp.handleBackPressed(this)) {
// we handled it
return;
}
}
}
// onBackPressed unhandled by us
super.onBackPressed();
}
This can be the same always. If you have multiple fragment areas, simply repeat the sequence for each one. If you have additional logic, integrate it before or after, but before you call super.onBackPressed to let the system take over (i.e. exit your activity).
Here is a sample of what a Fragment can do. This example uses a WebView and it wants to "use" the back key to manage the Back Stack of the WebView:
public class BrowseUrlFragment extends Fragment implements IHandleBackPressed {
WebView wv;
public boolean handleBackPressed(Activity ax) {
if(wv != null && wv.canGoBack()) {
wv.postDelayed(goback, 150);
return true;
}
return false;
}
}

How to persist the state of a custom Android View?

I want to manipulate a library I just discovered to drag/rotate and pinch zoom images. I am using two sources:
-The library itself: MultiTouchController.java https://code.google.com/p/android-multitouch-controller/source/browse/MTController/src/org/metalev/multitouch/controller/MultiTouchController.java
-An implementation of this library: PhotoSortrView https://code.google.com/p/android-multitouch-controller/source/browse/MTPhotoSortr/src/org/metalev/multitouch/photosortr/PhotoSortrView.java
-an example of Activity which implements this source:
public class PhotoSortrActivity extends Activity {
PhotoSortrView photoSorter;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setTitle(R.string.instructions);
photoSorter = new PhotoSortrView(this);
setContentView(photoSorter);
}
#Override
protected void onResume() {
super.onResume();
photoSorter.loadImages(this);
}
#Override
protected void onPause() {
super.onPause();
photoSorter.unloadImages();
}
#Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
photoSorter.trackballClicked();
return true;
}
return super.onKeyDown(keyCode, event);
}
}
The problem is the activity doesn't remember the position of the images when it leaves the foreground and I recall it after. Worse, the images are set randomly each time the Activity is called.
How can I make the Activity remember the state of the images? (the specific issue in my case is that it is a custom view)
Here is an example of what I want the Activity to remember, and then the reset made by the Activity after being called again:
See the savedInstanceState parameter in onCreate()? You override onSaveInstanceState() to set up a bundle that remembers your Activity state, and it shows up in the onCreate().
Now, if your question is: How do you save Activity state without changing the PhotoSortrView class; well, you can't. You would have to add some state parameter to loadImages() so that it loads the images and sets the current image, and maybe change unloadImages() so that it returns some state information that the Activity can save.

Close the current activity when you only have a reference to Context

If I have a reference to Context, is it possible to finish the current activity?
I don't have the reference to current activity.
yes, with a cast:
((Activity) ctx).finish();
In my Case following worked,
I need to finish my activity in a AsyncTask onPostExcute().
Where my AsyncTask class is separate public class , which has a constructor with param of Context.
((Activity)(mContext)).finish();
Only the above worked for me... Anyway I got this idea from #2red13 and #lucy answers... Thanks to all...
I know it's an old post but, perhaps it could be a good idea to call it this way:
if(context instanceof Activity){
((Activity)context).finish(); }
This way we make sure we don't get any unnecesary ClassCastExceptions
If you have access to the running view of the Activity you want to finish (for example, you are in a click listener), you could do:
((Activity)getContext()).finish();
(With thanks to 2red13 to get me here).
If you start the activity using:
startActivityForResult(i, 1);
you can call finishActivity(1) to finish any activities started with that request code, like this:
((Activity)getContext()).finishActivity(1);
In my case I need to use one in a handler postDelayed. Using this you can be sure of which activity you are finishing. Hope it helps!
I had the same problem when closing a preference activity. Here is what I did:
public class T2DPreferenceActivity extends PreferenceActivity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getFragmentManager().beginTransaction().replace(android.R.id.content,
new T2DPreferenceFragment()).commit();
}
public static class T2DPreferenceFragment extends PreferenceFragment {
#Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.server_screen_preference);
Preference testServicePreference = getPreferenceScreen().findPreference("PREFERRED SERVER");
testServicePreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
#Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
T2DPreferenceActivity.closeActivity(getActivity());
return true; //returning true still makes the activity handle the change to preferences
}
});
}
public void onViewCreated(View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
ListView lv = (ListView)view.findViewById(android.R.id.list);
ViewGroup parent = (ViewGroup)lv.getParent();
parent.setPadding(0, 100, 0, 0);
}
}
protected static void closeActivity(Activity activity) {
activity.finish();
}
}
Try:
((Activity) context.getApplicationContext()).finish();

Android WebView: handling orientation changes

The issue is the performance following rotation. The WebView has to reload the page, which can be a bit tedious.
What's the best way of handling an orientation change without reloading the page from source each time?
If you do not want the WebView to reload on orientation changes simply override onConfigurationChanged in your Activity class:
#Override
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig);
}
And set the android:configChanges attribute in the manifest:
<activity android:name="..."
android:label="#string/appName"
android:configChanges="orientation|screenSize"
for more info see:
http://developer.android.com/guide/topics/resources/runtime-changes.html#HandlingTheChange
https://developer.android.com/reference/android/app/Activity.html#ConfigurationChanges
Edit: This method no longer works as stated in the docs
Original answer:
This can be handled by overrwriting onSaveInstanceState(Bundle outState) in your activity and calling saveState from the webview:
protected void onSaveInstanceState(Bundle outState) {
webView.saveState(outState);
}
Then recover this in your onCreate after the webview has been re-inflated of course:
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.blah);
if (savedInstanceState != null)
((WebView)findViewById(R.id.webview)).restoreState(savedInstanceState);
}
The best answer to this is following Android documentation found here
Basically this will prevent Webview from reloading:
<activity android:name=".MyActivity"
android:configChanges="keyboardHidden|orientation|screenSize|layoutDirection|uiMode"
android:label="#string/app_name">
Edit(1/4/2020): You don't need this optional code, the manifest attribute is all you need, leaving optional code here to keep answer complete.
Optionally, you can fix anomalies (if any) by overriding onConfigurationChanged in the activity:
#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();
}
}
I've tried using onRetainNonConfigurationInstance (returning the WebView), then getting it back with getLastNonConfigurationInstance during onCreate and re-assigning it.
Doesn't seem to work just yet. I can't help but think I'm really close though! So far, I just get a blank/white-background WebView instead. Posting here in the hopes that someone can help push this one past the finish line.
Maybe I shouldn't be passing the WebView. Perhaps an object from within the WebView?
The other method I tried - not my favorite - is to set this in the activity:
android:configChanges="keyboardHidden|orientation"
... and then do pretty much nothing here:
#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.
}
THAT works, but it might not be considered a best practice.
Meanwhile, I also have a single ImageView that I want to automagically update depending on the rotation. This turns out to be very easy. Under my res folder, I have drawable-land and drawable-port to hold landscape/portrait variations, then I use R.drawable.myimagename for the ImageView's source and Android "does the right thing" - yay!
... except when you watch for config changes, then it doesn't. :(
So I'm at odds. Use onRetainNonConfigurationInstance and the ImageView rotation works, but WebView persistence doesn't ... or use onConfigurationChanged and the WebView stays stable, but the ImageView doesn't update. What to do?
One last note: In my case, forcing orientation isn't an acceptable compromise. We really do want to gracefully support rotation. Kinda like how the Android Browser app does! ;)
Best way to handle orientation changes and Preventing WebView reload on Rotate.
#Override
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig);
}
With that in mind, to prevent onCreate() from being called every time you change orientation, you would have to add android:configChanges="orientation|screenSize" to the AndroidManifest.
or just ..
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize"`
I appreciate this is a little late, however this is the answer that I used when developing my solution:
AndroidManifest.xml
<activity
android:name=".WebClient"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize" <--- "screenSize" important
android:label="#string/title_activity_web_client" >
</activity>
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.restoreState(savedInstanceState);
}
}
One compromise is to avoid rotation.
Add this to fix the activity for Portrait orientation only.
android:screenOrientation="portrait"
Just write the following code lines in your Manifest file - nothing else. It really works:
<activity android:name=".YourActivity"
android:configChanges="orientation|screenSize"
android:label="#string/application_name">
You can try using onSaveInstanceState() and onRestoreInstanceState() on your Activity to call saveState(...) and restoreState(...) on your WebView instance.
It is 2015, and many people are looking for a solution that still workds on Jellybean, KK and Lollipop phones.
After much struggling I found a way to preserve the webview intact after you change orientation.
My strategy is basically to store the webview in a separate static variable in another class. Then, if rotation occurs, I dettach the webview from the activity, wait for the orientation to finish, and reattach the webview back to the activity.
For example... first put this on your MANIFEST (keyboardHidden and keyboard are optional):
<application
android:label="#string/app_name"
android:theme="#style/AppTheme"
android:name="com.myapp.abc.app">
<activity
android:name=".myRotatingActivity"
android:configChanges="keyboard|keyboardHidden|orientation">
</activity>
In a SEPARATE APPLICATION CLASS, put:
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();
}
}
In the ACTIVITY put:
#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) {
restoreWebview();
}
public void restoreWebview(){
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);
}
Finally, the simple XML:
<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"
tools:context=".myRotatingActivity">
<FrameLayout
android:id="#+id/webviewplaceholder"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</RelativeLayout>
There are several things that could be improved in my solution, but I already spent to much time, for example: a shorter way to validate if the Activity has been launched for the very first time instead of using SharedPreferences storage.
This approach preserves you webview intact (afaik),its textboxes, labels, UI, javascript variables, and navigation states that are not reflected by the url.
The only thing you should do is adding this code to your manifest file:
<activity android:name=".YourActivity"
android:configChanges="orientation|screenSize"
android:label="#string/application_name">
Update: current strategy is to move WebView instance to Application class instead of retained fragment when it's detached and reattach on resume as Josh does.
To prevent Application from closing, you should use foreground service, if you want to retain state when user switches between applications.
If you are using fragments, you can use retain instance of the WebView.
The web view will be retained as instance member of the class. You should however attach web view in OnCreateView and detach before OnDestroyView to prevent it from destruction with the parent container.
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();
}
}
P.S. Credits go to kcoppock answer
As for 'SaveState()' it no longer works according to official documentation:
Please note that this method no longer stores the display data for
this WebView. The previous behavior could potentially leak files if
restoreState(Bundle) was never called.
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
}
#Override
protected void onRestoreInstanceState(Bundle state) {
super.onRestoreInstanceState(state);
}
These methods can be overridden on any activity, it just basically allows you to save and restore values each time an activity is created/destroyed, when the screen orientation changes the activity gets destroyed and recreated in the background, so therefore you could use these methods to temporary store/restore states during the change.
You should have a deeper look into the two following methods and see whether it fits your solution.
http://developer.android.com/reference/android/app/Activity.html
The best solution I have found to do this without leaking the previous Activity reference and without setting the configChanges.. is to use a MutableContextWrapper.
I've implemented this here: https://github.com/slightfoot/android-web-wrapper/blob/48cb3c48c457d889fc16b4e3eba1c9e925f42cfb/WebWrapper/src/com/example/webwrapper/BrowserActivity.java
This is the only thing that worked for me (I even used the save instance state in onCreateView but it wasn't as reliable).
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.restoreState(WebViewStateHolder.INSTANCE.getBundle());
}
return rootView;
}
}
I made a Singleton holder for the state of the WebView. The state is preserved as long as the process of the application exists.
EDIT: The loadDataWithBaseURL wasn't necessary, it worked just as well with just
//in onCreate() for Activity, or in onCreateView() for Fragment
if(WebViewStateHolder.INSTANCE.getBundle() == null) {
webView.loadUrl("file:///android_asset/html/merged.html");
} else {
webView.restoreState(WebViewStateHolder.INSTANCE.getBundle());
}
Although I read this doesn't necessarily work well with cookies.
try this
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.webkit.WebView;
import android.webkit.WebViewClient;
public class MainActivity extends AppCompatActivity {
private WebView wv;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
wv = (WebView) findViewById(R.id.webView);
String url = "https://www.google.ps/";
if (savedInstanceState != null)
wv.restoreState(savedInstanceState);
else {
wv.setWebViewClient(new MyBrowser());
wv.getSettings().setLoadsImagesAutomatically(true);
wv.getSettings().setJavaScriptEnabled(true);
wv.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
wv.loadUrl(url);
}
}
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
wv.saveState(outState);
}
#Override
public void onBackPressed() {
if (wv.canGoBack())
wv.goBack();
else
super.onBackPressed();
}
private class MyBrowser extends WebViewClient {
#Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
}
}
This page solve my problem but I have to make slight change in initial one:
protected void onSaveInstanceState(Bundle outState) {
webView.saveState(outState);
}
This portion has a slight problem for me this. On the second orientation change the application terminated with null pointer
using this it worked for me:
#Override
protected void onSaveInstanceState(Bundle outState ){
((WebView) findViewById(R.id.webview)).saveState(outState);
}
You should try this:
Create a service, inside that service, create your WebView.
Start the service from your activity and bind to it.
In the onServiceConnected method, get the WebView and call the setContentView method to render your WebView.
I tested it and it works but not with other WebViews like XWalkView or GeckoView.
#Override
protected void onSaveInstanceState(Bundle outState )
{
super.onSaveInstanceState(outState);
webView.saveState(outState);
}
#Override
protected void onRestoreInstanceState(Bundle savedInstanceState)
{
super.onRestoreInstanceState(savedInstanceState);
webView.restoreState(savedInstanceState);
}
I like this solution http://www.devahead.com/blog/2012/01/preserving-the-state-of-an-android-webview-on-screen-orientation-change/
According to it we reuse the same instance of WebView. It allows to save navigation history and scroll position on configuration change.

Categories

Resources