Android 7 WebView with wrap_content - android

I have a WebView with android:layout_height="wrap_content" inside a ScrollView. Prior to Android 7 this resulted in the WebView resizing to the height of the local html content I set with loadData. On my Nexus 5X with Android 7 though, the WebView height seems to be unreliable, sometimes it only shows parts of the first text line, sometimes there's a big gap at the end of the content.
I think it could be due to Google now using Chrome for WebViews starting with Nougat.
Does anyone have an explanation or fix/workaround for this issue?
It might also be important that the views are contained in the cells of a RecyclerView .

Workaround is:
waiting while html page will be loaded and run JS inside page to detect content height and set it to WebView Layout Parameters height.
It is easy to run JS inside page, just navigate to url like
javascript:Math.max(document.body.scrollHeight,document.body.offsetHeight,document.documentElement.clientHeight,document.documentElement.scrollHeight,document.documentElement.offsetHeight);
To pass the result to Java code you must provide Java-Java Script interface as described here https://developer.android.com/guide/webapps/webview.html (Binding JavaScript code to Android code)
Your url to navigate must looks like
javascript:myfunc(Math.max(document.body.scrollHeight,document.body.offsetHeight,document.documentElement.clientHeight,document.documentElement.scrollHeight,document.documentElement.offsetHeight));
The myfunc will be called and you will get the height of page. Now just set in height to WebView height.

Using WebView to showing contents inside a RecyclerView is a little bit risky and it is not so efficient. You have no idea how it works in different devices!
I suggest to change your solution using TextView and converting HTML contents to formatted text using this command
textView.setText(Html.fromHtml(<YOUR HTML CONTENT>))
By the way, These are some efforts I have done with WebView to achieve a fix size for showing some ads banner content:
public class AdsWebView extends WebView {
// some setup codes...
private static final int MAX_SCALE = 120;
public AdsWebView(Context context) {
setVerticalScrollBarEnabled(false);
setHorizontalScrollBarEnabled(false);
setScrollbarFadingEnabled(false);
setupAdsScale();
}
private int getAdsScale() {
int width = getDisplayWidth(getContext());
Double val = Double.valueOf(width) / Double.valueOf(480);
val = val * 100d;
return val.intValue();
}
private void setupAdsScale() {
int scale = getAdsScale();
if (scale > MAX_SCALE)
scale = MAX_SCALE;
setInitialScale(scale);
}
private int getDisplayWidth(Context context) {
try {
Activity activity = (Activity) context;
if (Integer.valueOf(android.os.Build.VERSION.SDK_INT) < 13) {
Display display = activity.getWindowManager()
.getDefaultDisplay();
return display.getWidth();
} else {
Display display = activity.getWindowManager()
.getDefaultDisplay();
Point size = new Point();
display.getSize(size);
return size.x;
}
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}

Related

Xamarin.Forms: Upgrading from 2.3.4 to 3.0 caused Android Contents to extend past bottom

I had to update a year-old app to the current version of forms. Unfortunately on Android, it seems to think the screen is taller then it actually is (see screen shot). Everything is working just fine for iOS, but all my pages are slightly taller than the screen on Android on multiple devices.
Some things I have tried:
Made sure I'm using the latest SDK's and latest Android Support files
Hacked in a paddingEv
Updated all my custom renderers with the new android constructors
Layout description:
Depends on the idiom: On a phone, it's a MasterDetail with a Tabbed Page as Detail, a Navigation Page for the first Child of Tabbed Page (so Menu appears for only that page), and Content Pages for the rest. For Tablet, the Children of the Tabbed pages are all Content Pages. Modal Pages are all Content Pages.
Any help, even diagnostically, would be greatly appreciated.
UPDATE:
My current hack is to adjust the padding in a special renderer like so:
public class HackLayoutShiftFixRenderer : MasterDetailPageRenderer
{
public HackLayoutShiftFixRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(VisualElement oldElement, VisualElement newElement)
{
base.OnElementChanged(oldElement, newElement);
if (!(newElement is MasterDetailPage masterDetail)) return;
masterDetail.Master.Padding = new Thickness(0, 0, 0, 24);
masterDetail.Detail.Padding = new Thickness(0, 0, 0, 24);
}
}
And I do something similar with modal pages. The number 24 was used with trial and error.
I found the answer, but wouldn't mind some clarification as to why this happened.
A year ago, this was implemented to fix the keyboard hiding search items on a Listview: http://xamarinformscorner.blogspot.com/2016/06/soft-keyboard-hiding-entries-fields-in.html
Code in case the link disappears:
public class AndroidBug5497WorkaroundForXamarinAndroid
{
// For more information, see https://code.google.com/p/android/issues/detail?id=5497
// To use this class, simply invoke assistActivity() on an Activity that already has its content view set.
// CREDIT TO Joseph Johnson (http://stackoverflow.com/users/341631/joseph-johnson) for publishing the original Android solution on stackoverflow.com
public static void assistActivity(Activity activity)
{
new AndroidBug5497WorkaroundForXamarinAndroid(activity);
}
private View mChildOfContent;
private int usableHeightPrevious;
private FrameLayout.LayoutParams frameLayoutParams;
private AndroidBug5497WorkaroundForXamarinAndroid(Activity activity)
{
FrameLayout content = (FrameLayout)activity.FindViewById(global::Android.Resource.Id.Content);
mChildOfContent = content.GetChildAt(0);
ViewTreeObserver vto = mChildOfContent.ViewTreeObserver;
vto.GlobalLayout += (object sender, EventArgs e) => {
possiblyResizeChildOfContent();
};
frameLayoutParams = (FrameLayout.LayoutParams)mChildOfContent.LayoutParameters;
}
private void possiblyResizeChildOfContent()
{
int usableHeightNow = computeUsableHeight();
if (usableHeightNow != usableHeightPrevious)
{
int usableHeightSansKeyboard = mChildOfContent.RootView.Height;
int heightDifference = usableHeightSansKeyboard - usableHeightNow;
frameLayoutParams.Height = usableHeightSansKeyboard - heightDifference;
mChildOfContent.RequestLayout();
usableHeightPrevious = usableHeightNow;
}
}
private int computeUsableHeight()
{
Rect r = new Rect();
mChildOfContent.GetWindowVisibleDisplayFrame(r);
if (Build.VERSION.SdkInt < BuildVersionCodes.Lollipop)
{
return (r.Bottom - r.Top);
}
return r.Bottom;
}
The problem was in the "computeUsableHeight" function: At least with the latest version, I must always take the difference between the top and bottom into account. I don't know why or when this changed between Xamarin.Forms 2.3.4 & 3.0

Changing MasterDetail Height With A Custom Renderer

I am currently trying to change the height of the default MasterDetailPage using the material design with FormsAppCompatActivity.
Basically I got the custom renderer to work, but I am trying to resize it dynamically based on toolbar size. The reason being is that different devices have different sized toolbars. Furthermore, I got the resizing to work but the black shadow that comes with the MasterDetailPage stays in place and does not seem to pass through the AddView function.
bool firstDone;
public override void AddView(Android.Views.View child)//Android.Views.View
{
var padding = child.GetType().GetRuntimeProperty("TopPadding").GetValue(child);//tried padding but did not work
if (firstDone)
{
LayoutParams p = (LayoutParams)child.LayoutParameters;
//p.TopMargin = padding;
p.TopMargin = 200;// Need this to be dynamic for different devices
base.AddView(child, p);
}
else
{
firstDone = true;
base.AddView(child);
}
}

Scroll effect for RecyclerView

I am using horizontal RecyclerView in Android app.
Showing two items (ImageViews) on the screen at a time.
To do this I am setting the width of each ImageView to half of the screen in ViewHolder class of the adapter:
public static class ViewHolder extends RecyclerView.ViewHolder {
private LinearLayout ll_Img;
private ImageView iv_ad;
private ViewHolder(View v) {
super(v);
ll_Img = (LinearLayout) itemView.findViewById(R.id.ll_Img);
ll_Img.getLayoutParams().width = (Utils.getScreenWidth(itemView.getContext()) / 2);
iv_ad = (ImageView) v.findViewById(R.id.iv_main_ad);
}
}
And getting screen's width:
public static int getScreenWidth(Context context) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
Point size = new Point();
display.getSize(size);
int screenWidth = size.x;
return screenWidth;
}
It works fine but I want to implement scroll effect like on iOS,
to scroll one item per swipe,
so after each swipe 2 items must fit the screen width.
By the way, two videos worth 1000 words
so here what I am having now:
https://drive.google.com/file/d/0B7j1Rf_oUEbLOWk1OUtpWXFpcEE/view?usp=sharing
And what I want to achieve (as my colleague implemented on iOS):
https://drive.google.com/file/d/0B6B-4-ITg1EQTElNTWsxMWg4aWs/view?usp=sharing
Each piece of advice is appreciated.
This is a viewpager, not a recyclerview. This link might help https://commonsware.com/blog/2012/08/20/multiple-view-viewpager-options.html
Just for anyone else's benefit:
getPageWidth() returns a floating-point number, between 0 and 1,
representing the portion of the width of the ViewPager that a given
page should take up. By default, the page width is 1, but by
overriding this, you can have multiple pages on the screen
simultaneously.
Not entirely sure if this would give the desired effect, but if you are using a linearlayoutmanager, you could add a scrolllistener to your recyclerview, and whenever it gets called, use the linearlayout's method scrollToPosition to force it to scroll all the way to the next item.

How do I scroll a WebView to a specific point on Android

I have a WebView and I would like my app to find a mark on the text(I'm using the symbol ► in the html).I have lots of html files each with almost 10 of these markings.
I have done it pretty easily with TextView doing this:
int offset=texto.indexOf("SPECIFIC MARKING ON TEXT");
final int line = textview.getLayout().getLineForOffset(offset);
final int y = textview.getLayout().getLineTop(line); // e.g. I want to scroll to line
final ScrollView s = (ScrollView)findViewById(R.id.ScrollView01);
s.post(new Runnable() {
#Override
public void run() {
s.smoothScrollTo(0, y);
}
});
But how do I accomplish that on Webview ?
The reason I use Webview is due to better text formatting.
In fact, it's even simpler with a WebView. You can use the findAll method for the first occurrence, then findNext(true) for the following markers. The view will automatically scroll to the requested text.

Maintain WebView content scroll position on orientation change

The browsers in Android 2.3+ do a good job at maintaining the scrolled position of content on an orientation changed.
I'm trying to achieve the same thing for a WebView which I display within an activity but without success.
I've tested the manifest change (android:configChanges="orientation|keyboardHidden") to avoid activity recreation on a rotation however because of the different aspect ratios the scroll position does not get set to where I want. Furthermore this is not a solution for me as I need to have different layouts in portrait and landscape.
I've tried saving the WebView state and restoring it but this resuls in the content being displayed at the top again.
Furthermore attempting to scroll in onPageFinished using scrollTo doesn't work even though the height of the WebView is non-zero at this point.
Any ideas? Thanks in advance. Peter.
Partial Solution:
My colleague managed to get scrolling working via a javascript solution. For simple scrolling to the same vertical position, the WebView's 'Y' scroll position is saved in onSaveInstanceState state. The following is then added to onPageFinished:
public void onPageFinished(final WebView view, final String url) {
if (mScrollY > 0) {
final StringBuilder sb = new StringBuilder("javascript:window.scrollTo(0, ");
sb.append(mScrollY);
sb.append("/ window.devicePixelRatio);");
view.loadUrl(sb.toString());
}
super.onPageFinished(view, url);
}
You do get the slight flicker as the content jumps from the beginning to the new scroll position but it is barely noticeable. The next step is try a percentage based method (based on differences in height) and also investigate having the WebView save and restore its state.
To restore the current position of a WebView during orientation change I'm afraid you will have to do it manually.
I used this method:
Calculate actual percent of scroll in the WebView
Save it in the onRetainNonConfigurationInstance
Restore the position of the WebView when it's recreated
Because the width and height of the WebView is not the same in portrait and landscape mode, I use a percent to represent the user scroll position.
Step by step:
1) Calculate actual percent of scroll in the WebView
// Calculate the % of scroll progress in the actual web page content
private float calculateProgression(WebView content) {
float positionTopView = content.getTop();
float contentHeight = content.getContentHeight();
float currentScrollPosition = content.getScrollY();
float percentWebview = (currentScrollPosition - positionTopView) / contentHeight;
return percentWebview;
}
2) Save it in the onRetainNonConfigurationInstance
Save the progress just before the orientation change
#Override
public Object onRetainNonConfigurationInstance() {
OrientationChangeData objectToSave = new OrientationChangeData();
objectToSave.mProgress = calculateProgression(mWebView);
return objectToSave;
}
// Container class used to save data during the orientation change
private final static class OrientationChangeData {
public float mProgress;
}
3) Restore the position of the WebView when it's recreated
Get the progress from the orientation change data
private boolean mHasToRestoreState = false;
private float mProgressToRestore;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mWebView = (WebView) findViewById(R.id.WebView);
mWebView.setWebViewClient(new MyWebViewClient());
mWebView.loadUrl("http://stackoverflow.com/");
OrientationChangeData data = (OrientationChangeData) getLastNonConfigurationInstance();
if (data != null) {
mHasToRestoreState = true;
mProgressToRestore = data.mProgress;
}
}
To restore the current position you will have to wait the page to be reloaded (
this method can be problematic if your page takes a long time to load)
private class MyWebViewClient extends WebViewClient {
#Override
public void onPageFinished(WebView view, String url) {
if (mHasToRestoreState) {
mHasToRestoreState = false;
view.postDelayed(new Runnable() {
#Override
public void run() {
float webviewsize = mWebView.getContentHeight() - mWebView.getTop();
float positionInWV = webviewsize * mProgressToRestore;
int positionY = Math.round(mWebView.getTop() + positionInWV);
mWebView.scrollTo(0, positionY);
}
// Delay the scrollTo to make it work
}, 300);
}
super.onPageFinished(view, url);
}
}
During my test I encounter that you need to wait a little after the onPageFinished method is called to make the scroll working. 300ms should be ok. This delay make the display to flick (first display at scroll 0 then go to the correct position).
Maybe there is an other better way to do it but I'm not aware of.
Why you should consider this answer over accepted answer:
Accepted answer provides decent and simple way to save scroll position, however it is far from perfect. The problem with that approach is that sometimes during rotation you won't even see any of the elements you saw on the screen before rotation. Element that was at the top of the screen can now be at the bottom after rotation. Saving position via percent of scroll is not very accurate and on large documents this inaccuracy can add up.
So here is another method: it's way more complicated, but it almost guarantees that you'll see exactly the same element after rotation that you saw before rotation. In my opinion, this leads to a much better user experience, especially on a large documents.
======
First of all, we will track current scroll position via javascript. This will allow us to know exactly which element is currently at the top of the screen and how much is it scrolled.
First, ensure that javascript is enabled for your WebView:
webView.getSettings().setJavaScriptEnabled(true);
Next, we need to create java class that will accept information from within javascript:
public class WebScrollListener {
private String element;
private int margin;
#JavascriptInterface
public void onScrollPositionChange(String topElementCssSelector, int topElementTopMargin) {
Log.d("WebScrollListener", "Scroll position changed: " + topElementCssSelector + " " + topElementTopMargin);
element = topElementCssSelector;
margin = topElementTopMargin;
}
}
Then we add this class to WebView:
scrollListener = new WebScrollListener(); // save this in an instance variable
webView.addJavascriptInterface(scrollListener, "WebScrollListener");
Now we need to insert javascript code into html page. This script will send scroll data to java (if you are generation html, just append this script; otherwise, you might need to resort to calling document.write() via webView.loadUrl("javascript:document.write(" + script + ")");):
<script>
// We will find first visible element on the screen
// by probing document with the document.elementFromPoint function;
// we need to make sure that we dont just return
// body element or any element that is very large;
// best case scenario is if we get any element that
// doesn't contain other elements, but any small element is good enough;
var findSmallElementOnScreen = function() {
var SIZE_LIMIT = 1024;
var elem = undefined;
var offsetY = 0;
while (!elem) {
var e = document.elementFromPoint(100, offsetY);
if (e.getBoundingClientRect().height < SIZE_LIMIT) {
elem = e;
} else {
offsetY += 50;
}
}
return elem;
};
// Convert dom element to css selector for later use
var getCssSelector = function(el) {
if (!(el instanceof Element))
return;
var path = [];
while (el.nodeType === Node.ELEMENT_NODE) {
var selector = el.nodeName.toLowerCase();
if (el.id) {
selector += '#' + el.id;
path.unshift(selector);
break;
} else {
var sib = el, nth = 1;
while (sib = sib.previousElementSibling) {
if (sib.nodeName.toLowerCase() == selector)
nth++;
}
if (nth != 1)
selector += ':nth-of-type('+nth+')';
}
path.unshift(selector);
el = el.parentNode;
}
return path.join(' > ');
};
// Send topmost element and its top offset to java
var reportScrollPosition = function() {
var elem = findSmallElementOnScreen();
if (elem) {
var selector = getCssSelector(elem);
var offset = elem.getBoundingClientRect().top;
WebScrollListener.onScrollPositionChange(selector, offset);
}
}
// We will report scroll position every time when scroll position changes,
// but timer will ensure that this doesn't happen more often than needed
// (scroll event fires way too rapidly)
var previousTimeout = undefined;
window.addEventListener('scroll', function() {
clearTimeout(previousTimeout);
previousTimeout = setTimeout(reportScrollPosition, 200);
});
</script>
If you run your app at this point, you should already see messages in logcat telling you that the new scroll position is received.
Now we need to save webView state:
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
webView.saveState(outState);
outState.putString("scrollElement", scrollListener.element);
outState.putInt("scrollMargin", scrollListener.margin);
}
Then we read it in the onCreate (for Activity) or onCreateView (for fragment) method:
if (savedInstanceState != null) {
webView.restoreState(savedInstanceState);
initialScrollElement = savedInstanceState.getString("scrollElement");
initialScrollMargin = savedInstanceState.getInt("scrollMargin");
}
We also need to add WebViewClient to our webView and override onPageFinished method:
#Override
public void onPageFinished(final WebView view, String url) {
if (initialScrollElement != null) {
// It's very hard to detect when web page actually finished loading;
// At the time onPageFinished is called, page might still not be parsed
// Any javascript inside <script>...</script> tags might still not be executed;
// Dom tree might still be incomplete;
// So we are gonna use a combination of delays and checks to ensure
// that scroll position is only restored after page has actually finished loading
webView.postDelayed(new Runnable() {
#Override
public void run() {
String javascript = "(function ( selectorToRestore, positionToRestore ) {\n" +
" var previousTop = 0;\n" +
" var check = function() {\n" +
" var elem = document.querySelector(selectorToRestore);\n" +
" if (!elem) {\n" +
" setTimeout(check, 100);\n" +
" return;\n" +
" }\n" +
" var currentTop = elem.getBoundingClientRect().top;\n" +
" if (currentTop !== previousTop) {\n" +
" previousTop = currentTop;\n" +
" setTimeout(check, 100);\n" +
" } else {\n" +
" window.scrollBy(0, currentTop - positionToRestore);\n" +
" }\n" +
" };\n" +
" check();\n" +
"}('" + initialScrollElement + "', " + initialScrollMargin + "));";
webView.loadUrl("javascript:" + javascript);
initialScrollElement = null;
}
}, 300);
}
}
This is it. After screen rotation, element that was at the top of your screen should now remain there.
onPageFinished may not be called because you are not reloading the page, you are just changing the orientation, not sure if this causes a reload or not.
Try using scrollTo in the onConfigurationChanged method of your activity.
The aspect change will most likely always cause the current location in your WebView to not be the right location to scroll to afterwards. You could be sneaky and determine the top most visible element in the WebView and after an orientation change implant an anchor at that point in the source and redirect the user to it...
to calculate the right percentage of WebView, it is important to count mWebView.getScale() also. Actually, return value of getScrollY() is respectively with mWebView.getContentHeight()*mWebView.getScale().
In the partial solution described in the original post, you can improve the solution if change the following code
private float calculateProgression(WebView content) {
float contentHeight = content.getContentHeight();
float currentScrollPosition = content.getScrollY();
float percentWebview = currentScrollPosition/ contentHeight;
return percentWebview;
}
due to the top position of the component, when you are using content.getTop () is not necessary; So what it does is add to the actual position of the scroll Y position where the component is located with respect to it parent and runs down the content. I hope my explanation is clear.
The way that we have managed to achieve this to have a local reference to the WebView within our Fragments.
/**
* WebView reference
*/
private WebView webView;
Then setRetainInstance to true in onCreate
#Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
// Argument loading settings
}
Then when onCreateView is called, return the existing local instance of the WebView, otherwise instantiate a new copy setting up the content and other settings. The key step he is when reattaching the WebView to remove the parent which you can see in the else clause below.
#Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
final View view;
if (webView == null) {
view = inflater.inflate(R.layout.webview_dialog, container);
webView = (WebView) view.findViewById(R.id.dialog_web_view);
// Setup webview and content etc...
} else {
((ViewGroup) webView.getParent()).removeView(webView);
view = webView;
}
return view;
}
I used the partial solution described in the original post, but instead put the javascript snippet inside onPageStarted(), instead of onPageFinished(). Seems to work for me with no jumping.
My use case is slightly different: I'm trying to keep the horizontal position after the user refreshes the page.
But I've been able to use your solution and it works perfectly for me :)

Categories

Resources