I know this has been asked too many times and that SO is full of similar questions. I went through most of them and I've been researching this issue for a couple of days and I have yet to find the definitive solution.
I could easily avoid all this trouble adding configChanges="orientation|screenSize" to the activity containing the WebView; I've tested this and it worked as intended (the WebView didn't reload). But I really wanted to avoid this solution.
Here's my current WebView implementation:
public class BrowserFragment extends Fragment {
private WebView mWebView;
public BrowserFragment() {
// Required empty public constructor
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (mWebView == null) {
mWebView = new WebView(getActivity());
}
return mWebView;
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mWebView.setWebViewClient(new WebViewClient() {
#Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return super.shouldOverrideUrlLoading(view, url);
}
});
mWebView.getSettings().setAllowFileAccess(true);
mWebView.getSettings().setBuiltInZoomControls(false);
mWebView.getSettings().setDomStorageEnabled(true);
mWebView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);
mWebView.getSettings().setJavaScriptEnabled(true);
mWebView.getSettings().setSupportZoom(false);
if (savedInstanceState == null) {
mWebView.loadUrl("http://developer.android.com/");
} else {
mWebView.restoreState(savedInstanceState);
}
}
#Override
public void onResume() {
mWebView.onResume();
super.onResume();
}
#Override
public void onPause() {
super.onPause();
mWebView.onPause();
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mWebView.saveState(outState);
}
#Override
public void onDestroyView() {
if (getRetainInstance() && mWebView.getParent() instanceof ViewGroup) {
((ViewGroup) mWebView.getParent()).removeView(mWebView);
}
super.onDestroyView();
}
}
As for the MainActivity, there's nothing there besides setting the content view to the following layout file:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="#+id/fragment_browser"
android:name="com.example.app.BrowserFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</merge>
Notice above that I'm retaining the fragment instance and I'm also saving the state of the WebView so I can late restore that same state. This is what the documentation has to say about the restoreState method:
Restores the state of this WebView from the given Bundle. This method
is intended for use in onRestoreInstanceState(Bundle) and should be
called to restore the state of this WebView. If it is called after
this WebView has had a chance to build state (load pages, create a
back/forward list, etc.) there may be undesirable side-effects. Please
note that this method no longer restores the display data for this
WebView.
Meaning, the display data will not be restored but things like the scroll position will (I also tested this and it's working nicely). That's why I am creating a new instance of the WebView, only if it's null during onCreateView and removing it from the parent view during onDestroyView. This was taken from this answer: https://stackoverflow.com/a/32801278/40480
This mechanism will make the rotate process a little bit more smooth (without it, a full blank page was shown) but won't prevent the WebView from still reloading the page.
Any ideas how can I possibly solve this problem?
I had a similar problem. It's a bit late but my workaround is similar than yours but adding the webview again to the layout in onCreateView:
create new WebView if instance is null (first time) and add it to layout (in my case parent is a FrameLayout, at position 0).
if already created (activity recreated) just add to frame and invalidate (important to redraw webview on rotation)
mFrame = (FrameLayout) mView.findViewById(R.id.dq_fragment_browser_common_frame);
if (mWebView == null){
mWebView = new CommonWebView(getContext());
mFrame.addView(mWebView,0);
} else {
mFrame.addView(mWebView,0);
mWebView.invalidate();
}
As in your case I remove webview from frame in OnDestroyView() to be able to add it again in onCreateView:
#Override
public void onDestroyView() {
mFrame.removeView(mWebView);
super.onDestroyView();
}
NOTE: I don't save and store the webview state as it reloads the page (at least in my case). Furthermore, the scroll position does not normally match in portrait/landscape but same problem in Chrome as far as I've checked.
Regards.
Related
Hello I've a problem when using my app on the phone, when I rotate the screen inside the app the webview reloads all content getting back to the first page we were opening the app.
I leave you a gif of what happens here and if you know a solution please help me. I also leave my main class and the android manifest.
Proof Video
Main.java
AndroidManifest.xml
Add android:configChanges="orientation|screenSize" in your Manifest.xml file
Like below:
<activity
android:name=".MyActivity"
android:configChanges="orientation|screenSize"
android:label="#string/title_activity"
android:theme="#style/AppTheme.MyTheme">
</activity>
Create a fragment to wrap your WebView up. Always return the same WebView in onCreateView, so that a new WebView would not be created on orientation change, thus not reloading.
public class MyWebViewFragment extends Fragment {
private WebView mWebView;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
#Override
public void onResume() {
super.onResume();
mWebView.onResume();
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (mWebView == null)
mWebView = new WebView(getActivity());
ViewGroup parent = (ViewGroup) mWebView.getParent();
if (parent != null)
parent.removeView(mWebView);
return mWebView;
}
}
Add this fragment to your activity.
We all know that when using ViewPager with Fragment and FragmentPagerAdapter we get 3 Fragment loaded: the visible one, and both on each of its sides.
So, if I have 7 Fragments and I'm iterating through them to see which 3 of them are the ones that are loaded, and by that I mean onCreateView() has already been called, how can I determine this?
EDIT: The Fragment doesn't have to be the one that the ViewPager is showing, just that onCreateView() has already been called.
Well logically, this would be a reasonable test if onCreateView has been called:
myFragment.getView() != null;
Assuming you a have a reference to all of the fragments in the pager iterate, them and check if they have a view.
http://developer.android.com/reference/android/app/Fragment.html#getView()
Update
The above answer assumes that your fragments always create a view, and are not viewless fragments. If they are then I suggest sub classing the fragment like so:
public abstract class SubFragment extends Fragment
{
protected boolean onCreateViewCalled = false;
public boolean hasOnCreateViewBeenCalled()
{
return onCreateViewCalled;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup Container, Bundle state){
onCreateViewCalled = true;
return null;
}
}
Just bear in mind that further sub classes will have to call super or set the flag themselves should they override onCreateView as well.
I added an interface to Fragment. Looks like:
protected OnCreateViewCallback createViewCallback = null;
public void setCreateViewCallback(OnCreateViewCallback createViewCallback) {
this.createViewCallback = createViewCallback;
}
public interface OnCreateViewCallback {
void onCreateView();
}
In my onCreateView():
//initialize your view.
if (createViewCallback != null) {
createViewCallback.onCreateView();
createViewCallback = null;
}
return mainView;
From my activity:
if (ocrFragment.getView() == null) {
ocrFragment.setCreateViewCallback(new MainScreenFragment.OnCreateViewCallback() {
#Override
public void onCreateView() {
ocrFragment.ocrImage(picture, false);
}
});
} else {
ocrFragment.ocrImage(picture, false);
}
If you are trying to perform something after onCreateView is called, use onViewCreated:
Called immediately after onCreateView(LayoutInflater, ViewGroup,
Bundle) has returned, but before any saved state has been restored in
to the view. This gives subclasses a chance to initialize themselves
once they know their view hierarchy has been completely created. The
fragment's view hierarchy is not however attached to its parent at
this point.
public void onViewCreated(View view, Bundle savedInstanceState) {
MyActivity myActivity = (MyActivity) getActivity();
MyActivity.newAsyncTask(mPar);
}
You could also check for Fragment.isVisible() because a Fragment is in visible state when it's in the offscreen page limit of a ViewPager.
Edit: But it just really depends on what you really want to achieve with your question. Perhaps some kind of update to all UIs in your Fragments when their UI is ready?
EDIT:
Just another addition, you could listen to onViewCreated() and set a flag. Or notify your Activity and do further work (getActivity() will return your Activity at this point). But really, better state what you want to accomplish with your question.
What I want is, that when the device is rotated, FragmentActivity is being still kept.
Im using a WebView in a FragmentActivity and I have configured android:configChanges="orientation|keyboardHidden" for the FragmentActivity in the Manifest.xml - but when I rotate the device, the Activity crashes.
main.java:
#Override
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig);
}
tab1.java:
public class Tab1 extends Fragment {
Context mContext;
WebView web ;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View mainView = (View) inflater.inflate(R.layout.activity_tab1, container, false);
web = (WebView) mainView.findViewById(R.id.webview1);
web.setWebViewClient(new WebClient());
WebSettings settings = web.getSettings();
settings.setJavaScriptEnabled(true);
settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
web.loadUrl("http://google.com");
//웹뷰에서 뒤로가기
web.setOnKeyListener(new OnKeyListener(){
public boolean onKey(View v, int keyCode, KeyEvent event) {
if ((keyCode == KeyEvent.KEYCODE_BACK) && web.canGoBack()) {
handler.sendEmptyMessage(1);
return true;
}
return false;
}
});
return mainView;
}
}
I want to keep webview every time I rotate my phone.
Any advise about it?
Something to keep in mind, is on rotate the app will call onCreate. So it is essentially recreating your app every time you rotate. So to fix this; (I think) you need to save the state of your app in savedInstanceState, then after rotate just resume your state from that.
If you already specified in the manifest that you will handle orientation changed, and you don't want to go through the regular life cycle, in the configuration change do something like this:
#Override
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(null);
}
Also if all you want is to retain the instance of your fragment, you could call the method:
Fragment.setRetainInstance(true);
Which as per googles Documentation "Controls whether a fragment instance is retained across Activity re-creation (such as from a configuration change)."
Regards!
I have a simple layout containing a VideoView.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#color/black"
android:gravity="center" >
<VideoView
android:id="#+id/videoPlayer"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>
</RelativeLayout>
The Activity that uses this layout creates a Fragment to start the VideoView
public class VideoPlayerActivity extends Activity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_video_player);
createNewWorkerFragment();
}
private void createNewWorkerFragment() {
FragmentManager fragmentManager = getFragmentManager();
VideoPlayerActivityWorkerFragment workerFragment = (VideoPlayerActivityWorkerFragment)fragmentManager.findFragmentByTag(VideoPlayerActivityWorkerFragment.Name);
if (workerFragment == null) {
workerFragment = new VideoPlayerActivityWorkerFragment();
fragmentManager.beginTransaction()
.add(workerFragment, VideoPlayerActivityWorkerFragment.Name)
.commit();
}
}
}
The VideoPlayerActivityWorkerFragment is as follows:
public class VideoPlayerActivityWorkerFragment extends Fragment {
public static String Name = "VideoPlayerActivityWorker";
private VideoView mVideoPlayer;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
mVideoPlayer = (VideoView) mActivity.findViewById(R.id.videoPlayer);
mVideoPlayer.setVideoPath(mActivity.getIntent().getExtras().getString("path"));
MediaController controller = new MediaController(mActivity);
controller.setAnchorView(mVideoPlayer);
mVideoPlayer.setMediaController(controller);
mVideoPlayer.requestFocus();
mVideoPlayer.start();
}
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mActivity = activity;
}
#Override
public void onDetach() {
super.onDetach();
mActivity = null;
}
}
This is the issue I'm having, when the VideoPlayerActivity starts the VideoPlayerActivityWorkerFragment is created and the VideoView starts playing, however when I rotate the device the video stops and will not play, the entire View seems gone from the layout. Due to setRetainInstance(true); I thought that the VideoView would continue to play. Can someone let me know what I'm doing wrong? I have used this pattern elsewhere (not with a VideoView) and it successfully allows rotation to happen.
I am unwilling to use the android:configChanges="keyboardHidden|orientation|screenSize" or similar methods, I would like to handle the orientation change with Fragments.
Thank you in advance for your help.
Edit
I ended up picking the solution I did because it works. The videoview and controller need to be recreated each time onCreateView is called and the playback position needs to be set in onResume and recored in onPause. However, the playback is choppy during the rotation. The solution is not optimal but it does work.
See mVideoPlayer = (VideoView) mActivity.findViewById(R.id.videoPlayer);
Your VideoView is created by the activity which is being destroyed and recreated on rotation. This means a new R.id.videoPlayer VideoView is being created. So your local mVideoPlayer is just being overwritten in your above line. Your fragment needs to create the VideoView in its onCreateView() method. Even then, this may not suffice. Because Views are inherently linked with their owning Context. Perhaps an explicit pause, detect and attach, play of the view would be a better way to go.
Here are some observations:
1.First of all, you should know that even if a fragment is retained the activity gets distroyed.
As a result, the onCreate() method of activity, where you add the fragment, is called every time thus resulting in multple fragments overlaping each other.
In VideoPlayerActivity you need a mechanism to determine whether the activity is created for the first time, or is re-created due to a configuration change.
You can solve this problem by putting a flag in onSaveInstanceState() and then checking the savedInstanceState in onCreate(). If it's null, it means the activity is created for the first time, so only now you can add the fragment.
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
if (savedInstanceState == null) {
createNewWorkerFragment();
}
}
// ....
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean("fragment_added", true);
}
2.Secondly, in VideoPlayerActivityWorkerFragment you are refering to the VideoView declared in the activity:
mVideoPlayer = (VideoView) mActivity.findViewById(R.id.videoPlayer);
which gets destroyed when the screen is rotated.
What you should do, is to extract that VideoView and put it in a separate layout file that will be inflated by the fragment in the onCreateView() method.
But even here you may have some troubles. Despide the fact that setRetainInstance(true) is called, onCreateView() is called every time on screen orientation, thus resulting in re-inflation of your layout.
The solution would be to inflate the layout in onCreateView() only once:
public class VideoPlayerActivityWorkerFragment extends Fragment {
private View view;
//.....
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (view == null) {
view = inflater.inflate(R.layout.my_video_view, container, false);
mVideoPlayer = (VideoView) view.findViewById(R.id.videoPlayer);
// .....
} else {
// If we are returning from a configuration change:
// "view" is still attached to the previous view hierarchy
// so we need to remove it and re-attach it to the current one
ViewGroup parent = (ViewGroup) view.getParent();
parent.removeView(view);
}
return view;
}
}
}
If all the above said does not make sense for you, reserve some time to play with fragments, especially inflation of a layout in a fragment, then come back and read this answer again.
I currently have my application set up with a ListFragment on the left and a DetailsFragment on the right (similar to the layout on the tablet below).
On the details fragment (fragment next to the list) I have a goto deal button, which when pressed should replace the detailsFragment with a WebViewFragment.
The problem I am having is that when trying to load a url in the webviewfragment the WebView is null.
WebViewFragment webViewFragment = new WebViewFragment();
FragmentTransaction transaction = getFragmentManager().beginTransaction();
// Replace whatever is in the fragment_container view with this fragment,
// and add the transaction to the back stack
transaction.replace(R.id.deal_details_fragment, webViewFragment);
transaction.addToBackStack(null);
// Commit the transaction
transaction.commit();
// Set the url
if (webViewFragment.getWebView()==null)
Log.d("webviewfragment", "is null");
webViewFragment.getWebView().loadUrl("http://www.google.com");
Below is my main layout which has the original two fragments defined.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/main_activity_layout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal" >
<fragment
android:name="com.bencallis.dealpad.DealListFragment"
android:id="#+id/deal_list_fragment"
android:layout_weight="1"
android:layout_width="0px"
android:layout_height="match_parent" >
<!-- Preview: layout=#layout/deal_list_fragment -->
</fragment>
<fragment
android:name="com.bencallis.dealpad.DealDetailsFragment"
android:id="#+id/deal_details_fragment"
android:layout_weight="2"
android:layout_width="0px"
android:layout_height="match_parent" >
<!-- Preview: layout=#layout/deal_details_fragment -->
</fragment>
</LinearLayout>
It seems that the webViewFragment is not being created fully as the WebView has not been initialised. I have looked online but there is very little information regarding the WebViewFragment.
Any ideas how to ensure WebView is initialised in the WebViewFragment?
With great help from Espiandev I have managed to get a working WebView. To ensure that links opened in the fragment and not in a web browser application I created a simple InnerWebView client which extends WebViewClinet.
public class DealWebViewFragment extends Fragment {
private WebView mWebView;
private boolean mIsWebViewAvailable;
private String mUrl = null;
/**
* Creates a new fragment which loads the supplied url as soon as it can
* #param url the url to load once initialised
*/
public DealWebViewFragment(String url) {
super();
mUrl = url;
}
/**
* Called to instantiate the view. Creates and returns the WebView.
*/
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
if (mWebView != null) {
mWebView.destroy();
}
mWebView = new WebView(getActivity());
mWebView.setOnKeyListener(new OnKeyListener(){
#Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if ((keyCode == KeyEvent.KEYCODE_BACK) && mWebView.canGoBack()) {
mWebView.goBack();
return true;
}
return false;
}
});
mWebView.setWebViewClient(new InnerWebViewClient()); // forces it to open in app
mWebView.loadUrl(mUrl);
mIsWebViewAvailable = true;
WebSettings settings = mWebView.getSettings();
settings.setJavaScriptEnabled(true);
return mWebView;
}
/**
* Convenience method for loading a url. Will fail if {#link View} is not initialised (but won't throw an {#link Exception})
* #param url
*/
public void loadUrl(String url) {
if (mIsWebViewAvailable) getWebView().loadUrl(mUrl = url);
else Log.w("ImprovedWebViewFragment", "WebView cannot be found. Check the view and fragment have been loaded.");
}
/**
* Called when the fragment is visible to the user and actively running. Resumes the WebView.
*/
#Override
public void onPause() {
super.onPause();
mWebView.onPause();
}
/**
* Called when the fragment is no longer resumed. Pauses the WebView.
*/
#Override
public void onResume() {
mWebView.onResume();
super.onResume();
}
/**
* Called when the WebView has been detached from the fragment.
* The WebView is no longer available after this time.
*/
#Override
public void onDestroyView() {
mIsWebViewAvailable = false;
super.onDestroyView();
}
/**
* Called when the fragment is no longer in use. Destroys the internal state of the WebView.
*/
#Override
public void onDestroy() {
if (mWebView != null) {
mWebView.destroy();
mWebView = null;
}
super.onDestroy();
}
/**
* Gets the WebView.
*/
public WebView getWebView() {
return mIsWebViewAvailable ? mWebView : null;
}
/* To ensure links open within the application */
private class InnerWebViewClient extends WebViewClient {
#Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
}
Hopefully this is useful to someone in the future.
EDIT: So I played around with this for a while and it seems that the WVF is a bit rubbish and designed to be overridden. However, there's no documentation on this at all! The problem stems from the fact you can call getWebView() before the Fragments view is loaded, hence your NullPointerException. Except there isn't any way to detect when the Fragment's view has been loaded, so you're kind of stuck!
Instead I overrode the class, adding bits and changing bits, so that now it will work fine.
Check this link for the code. Then instead of using:
WebViewFragment webViewFragment = new WebViewFragment();
to load your Fragment, use:
ImprovedWebViewFragment wvf = new ImprovedWebViewFragment("www.google.com");
This class also includes a convenience method for loading a url, that won't throw an Exception if there's no WebView.
So, no, I don't think there's a particularly simple way for using the built-in WebViewFragment, but it is pretty easy to make something that works instead. Hope it helps!
WebViewFragment as is is not that straightforward to use. Try this simple extension (You can copy/paste):
public class UrlWebViewFragment extends WebViewFragment{
private String url;
public static UrlWebViewFragment newInstance(String url) {
UrlWebViewFragment fragment = new UrlWebViewFragment();
fragment.url = url;
return fragment;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
WebView webView = (WebView) super.onCreateView(inflater, container, savedInstanceState);
webView.loadUrl(url);
return webView;
}
}
Call where you need using the factory method:
WebViewFragment fragment = UrlWebViewFragment.newInstance("http://ur-url.com");
Fragments can only be replaced if they were initiallized in Java, not XML. I think so, I had the same problem and it solved it. Change your XML to this:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/main_activity_layout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal" >
<fragment
android:name="com.bencallis.dealpad.DealListFragment"
android:id="#+id/deal_list_fragment"
android:layout_weight="1"
android:layout_width="0px"
android:layout_height="match_parent" >
<!-- Preview: layout=#layout/deal_list_fragment -->
</fragment>
<View
android:id="#+id/my_container"
android:layout_weight="2"
android:layout_width="0px"
android:layout_height="match_parent" >
</View>
</LinearLayout>
and then in Java, your onCreate method:
FragmentTransaction transaction = getFragmentManager().beginTransaction();
transaction.replace(R.id.my_container, new DealDetailsFragment());
transaction.commit();
or even better create whole method to just deal with Transactions.
Now Transaction from your question should work. :)