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 :)
Related
I have an image inside of my web view. The image is so big and I've enabled scrolls but the problem is that I want to set default view (first look on image before doing scroll or zoom ) to specific part of image. Any idea ?
WebView callUsView2= (WebView) rootView.findViewById(R.id.callUsView2);
WebSettings callUsView2Setting = callUsView2.getSettings();
callUsView2Setting.setBuiltInZoomControls(true);
callUsView2Setting.setDisplayZoomControls(true);
callUsView2.setVerticalScrollBarEnabled(true);
callUsView2.setHorizontalScrollBarEnabled(true);
callUsView2.loadUrl("file:///android_asset/map.png");
Result >
What I want >
ANSWER >>
callUsView2.setWebChromeClient(new WebChromeClient(){
#Override
public void onProgressChanged(WebView view, int progress) {
if ( view.getProgress()==100) {
// I save Y w/in Bundle so orientation changes [in addition to
// initial loads] will reposition to last location
jumpToY(1000 , 1800 );
}
}
private void jumpToY (final int xLocation , final int yLocation ) {
callUsView2.postDelayed( new Runnable () {
#Override
public void run() {
callUsView2.scrollTo(xLocation, yLocation);
}
}, 300);
}
} );
If the image is big and you want to show a specific part, you may want to zoom and scroll to a specific part.
What you can do is for example change the scale:
callUsView2.setInitialScale(some value which fits your needs);
and scroll if you wish to change the position:
callUsView2.scrollTo(x,y);
I have used custom coveflow, everything works fine when i load small amount of data, but it doesn't work with large amount of data,
check it out my below code
if (mAdapter == null || mAdapter.getCount() == 0)
throw new IllegalStateException(
"You are trying to scroll container with no adapter set. Set adapter first.");
if (mLastCenterItemIndex != -1) {
final int lastCenterItemPosition = (mFirstItemPosition + mLastCenterItemIndex)
% mAdapter.getCount();
final int di = lastCenterItemPosition - position;
final int dst = (int) (di * mCoverWidth * mSpacing);
mScrollToPositionOnNextInvalidate = -1;
scrollBy(-dst, 0);
} else {
mScrollToPositionOnNextInvalidate = position;
}
invalidate();
i have used this code to move my item to center of the tablet, now i am gng to explain my view, in one activity half of my screen occupies coverflow and other half occupies mapview, when i click map marker icon i need to move particular item to center of my screen in coverflow, so basically my coverflow and map sync, the changes must affect both,
it works perfectly when i load small amount of data, but now i tried to load 11000 records when i click marker then my UI gets blocked becuase of scrollby in Coverflow, can you suggest any idea to move my item center?? or is there any method which doesn't affect UI
All suggestion are most welcome
Thanks
Just include your code in
new Thread(new Runnable() {
public void run () {
//.........
}
}).start();
or load items by parts
I have an app that loads HTML content in three WebViews. For simplicity, let's call them top, middle, and bottom.
The user is always viewing the middle WebView. When the user reaches the top of the page and swipes down, the layout of the three WebViews change so that the top view is visible. Conversely, when the user reaches the bottom of the page and swipes up, the bottom page comes into view.
Imagine a 100x100 screen. Coordinate 0,0 is the top left of the screen and 100,100 is the bottom right of the screen. The top view will have a layout with the top at -105 so that it is not viewable, the middle view will occupy the screen, and the bottom view will have a layout with the top at 105 so that it is not viewable, as well.
topLayout.topMargin = -105;
middleLayout.topMargin = 0;
bottomLayout.topMargin = 105;
top.setLayoutParams(topLayout);
middle.setLayoutParams(middleLayout);
bottom.setLayoutParams(bottomLayout);
The content are books, so when the user changes pages, the content should flow logically. When scrolling backwards (up), the bottom of the previous page should be shown. When scrolling forwards (down), the top of the next page should be shown. This is accomplished through setting the WebView's scroll positions using scrollTo(x,y). The code looks like this, where a represents the bottom of the content and b represents the top of the content:
top.scrollTo(0, a);
middle.scrollTo(0, {a,b}); // a for previous page; b for next page
bottom.scrollTo(0, b);
When the user swipes to the previous page, the top WebView's layout changes to have a top of 0 to occupy the screen; the middle changes to 105, and the bottom changes to -105 and loads different content so the app will be prepared for a future previous swipe.
Now we actually come to my question. This works exactly as intended except in Android 4.4 (KitKat). In KitKat, it works for two swipes in either direction, but then on the third and subsequent swipe in the same direction, the content is loaded in the wrong position. When scrolling backwards, the content starts to load showing the top. When scrolling forwards, the content starts to load showing the bottom.
I have stepped through the debugger and noticed that the layouts are set properly, followed by the scroll positions being set correctly. Then, after those values are set, but before the content is actually drawn, something happens in the stack that changes the scroll position.
This is where I'm totally lost. Why are the scroll values getting set correctly, then magically changing before the screen is drawn?
I already tried using an onLayoutCompleteListener, it didn't work. I will update the list of things attempted as I receive answers and try the suggestions. Thank you in advance for any and all help.
Here's a summary of what I'm doing to change pages:
public class MyWebView extends WebView {
// Assume this is instantiated; it is by the time it is needed
private List<MyWebView> viewArray = new List<MyWebView>(3);
private class CustomGestureListener extends
GestureDetector.SimpleOnGestureListener {
private static final String DEBUG_TAG = "Gestures";
private int scrollYOnTouch;
private int scrollYOnRelease;
#Override
public boolean onFling(MotionEvent event1, MotionEvent event2,
float velocityX, float velocityY) {
Log.d(DEBUG_TAG, "onFling: " + event1.toString()
+ event2.toString());
Log.i(DEBUG_TAG, "onFling: vX[" + velocityX
+ "], vY[" + velocityY + "]");
scrollYOnRelease = getScrollY();
int bottomOfPage = scrollYOnTouch + getMeasuredHeight();
int endOfContent = (int) Math.floor(getContentHeight() * getScale());
int proximity = endOfContent - bottomOfPage;
boolean atBottom = proximity <= 1;
Log.i(DEBUG_TAG, "atBottom = (" + proximity + " <= 1)");
if ((velocityY > VELOCITY_THRESHOLD)
&& (scrollYOnRelease <= 0) && (scrollYOnTouch == 0)) {
// User flung down while at the top of the page.
// Go to the previous page.
changePages(PREVIOUS_PAGE);
} else if ((velocityY < -VELOCITY_THRESHOLD)
&& (scrollYOnRelease >= scrollYOnTouch) && atBottom) {
// User flung up while at the bottom of the page.
// Go to the next page.
changePages(NEXT_PAGE);
}
return true;
}
} // end of CustomGestureListener
#Override
public boolean onTouchEvent(MotionEvent event) {
// Send the event to our gesture detector
// If it is implemented, there will be a return value
this.mDetector.onTouchEvent(event);
// If the detected gesture is unimplemented, send it to the superclass
return super.onTouchEvent(event);
}
#Override
public void changePages(int direction) {
int screenWidth = getScreenWidth();
int screenHeight = getScreenHeight();
VerticalPagingWebView previous;
VerticalPagingWebView current;
VerticalPagingWebView next;
if (direction == NEXT_PAGE) {
// Rearrange elements in webview array
// Next page becomes current page,
// current becomes previous,
// previous becomes next.
Collections.swap(viewArray, 0, 1);
Collections.swap(viewArray, 1, 2);
previous = viewArray.get(0);
current = viewArray.get(1);
next = viewArray.get(2);
// Prepare the next page
next.loadData(htmlContent, "text/html", null);
} else if (direction == PREVIOUS_PAGE) {
// Rearrange elements in webview array
// Previous page becomes current page,
// current becomes next,
// next becomes previous.
Collections.swap(viewArray, 1, 2);
Collections.swap(viewArray, 0, 1);
previous = viewArray.get(0);
current = viewArray.get(1);
next = viewArray.get(2);
// Prepare the previous page
previous.loadData(htmlContent, "text/html", null);
}
LayoutParams previousLayout = (LayoutParams) previous.getLayoutParams();
previousLayout.leftMargin = LEFT_MARGIN;
previousLayout.topMargin = -screenHeight - TOP_MARGIN;
previous.setLayoutParams(previousLayout);
LayoutParams currentLayout = (LayoutParams) current.getLayoutParams();
currentLayout.leftMargin = LEFT_MARGIN;
currentLayout.topMargin = 0;
current.setLayoutParams(currentLayout);
LayoutParams nextLayout = (LayoutParams) next.getLayoutParams();
nextLayout.leftMargin = LEFT_MARGIN;
nextLayout.topMargin = screenHeight + TOP_MARGIN;
next.setLayoutParams(nextLayout);
previous.scrollToBottom();
next.scrollToTop();
// I'm unsure if this is needed, but it works on everything but KitKat
if (direction == NEXT_PAGE) {
current.scrollToTop();
} else {
current.scrollToBottom();
}
} // end of changePages
public void scrollToPageStart() {
scrollTo(0,0);
}
public void scrollToPageBottom() {
// I know getScale() is deprecated; I take care of it.
// This method works fine.
int endOfContent = (int) Math.floor(getContentHeight() * getScale());
int webViewHeight = getMeasuredHeight();
scrollTo(0, endOfContent - webViewHeight);
}
}
You're probably scrolling the WebView before it had finished loading the contents. The problem is that at some point during the page load the WebView resets the scroll (this is intentional, when you navigate between pages you don't want the scroll offset to persist) and sometimes this reset happens after you call scrollTo.
To fix this you have two options:
scroll from JavaScript (in window.onload or something), this ensures the scroll happens after the WebView has finished loading the contents,
wait for the contents to load before scrolling. This is harder since the WebView doesn't have reliable callbacks (onPageFinished will not work reliably). One option would be to poll for when the webview.getContentHeight() method is returning the height of your content before doing the scroll.
I'm generating a book app, of sorts, that displays pages in a WebView. I have Next/Previous ImageButtons and a GestureOverlay to detect left/right swipes.
When I want a page change I call:
private void changePage(int delta) {
currentPageIndex = currentPageIndex + delta;
if (currentPageIndex < 0) {
// negative index
currentPageIndex = 0;
} else if (currentPageIndex >= listOfPages.length) {
// index requested is out of range of the list
currentPageIndex = listOfPages.length - 1;
} else {
// set values for page load
filename = listOfPages[currentPageIndex];
mWebView.loadUrl("file:///android_asset/" + filename);
}
}
'listOfPages' is a string array of my filenames and loadUrl() works great, but is there any way that anyone knows of to be able to have a page transition to simulate a simple page turn?
If anyone's interested, I found a way to do this.
I defined Animation variables:
Animation slideLeftAnimation = AnimationUtils.loadAnimation(getBaseContext(), R.anim.slide_left);
Animation slideRightAnimation = AnimationUtils.loadAnimation(getBaseContext(), R.anim.slide_right);
And the slide_left and slide_right xml files are from the Android API tutorials.
Then, for left or right swipes, I used mWebView.startAnimation(leftOrRightAnimation); before my mWebView.loadUrl(url); call. Hope this helps anyone else! Chris
What's the best way to check if the view is visible on the window?
I have a CustomView which is part of my SDK and anybody can add CustomView to their layouts. My CustomView is taking some actions when it is visible to the user periodically. So if view becomes invisible to the user then it needs to stop the timer and when it becomes visible again it should restart its course.
But unfortunately there is no certain way of checking if my CustomView becomes visible or invisible to the user. There are few things that I can check and listen to: onVisibilityChange //it is for view's visibility change, and is introduced in new API 8 version so has backward compatibility issue
onWindowVisibilityChange //but my CustomView can be part of a ViewFlipper's Views so it can pose issues
onDetachedFromWindows //this not as useful
onWindowFocusChanged //Again my CustomView can be part of ViewFlipper's views. So if anybody has faced this kind of issues please throw some light.
In my case the following code works the best to listen if the View is visible or not:
#Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
Log.e(TAG, "is view visible?: " + (visibility == View.VISIBLE));
}
onDraw() is called each time the view needs to be drawn. When the view is off screen then onDraw() is never called. When a tiny bit of the view is becomes visible to the user then onDraw() is called. This is not ideal but I cannot see another call to use as I want to do the same thing. Remember to call the super.onDraw or the view won't get drawn. Be careful of changing anything in onDraw that causes the view to be invalidate as that will cause another call to onDraw.
If you are using a listview then getView can be used whenever your listview becomes shown to the user.
obviously the activity onPause() is called all your views are all covered up and are not visible to the user. perhaps calling invalidate() on the parent and if ondraw() is not called then it is not visible.
This is a method that I have used quite a bit in my apps and have had work out quite well for me:
static private int screenW = 0, screenH = 0;
#SuppressWarnings("deprecation") static public boolean onScreen(View view) {
int coordinates[] = { -1, -1 };
view.getLocationOnScreen(coordinates);
// Check if view is outside left or top
if (coordinates[0] + view.getWidth() < 0) return false;
if (coordinates[1] + view.getHeight() < 0) return false;
// Lazy get screen size. Only the first time.
if (screenW == 0 || screenH == 0) {
if (MyApplication.getSharedContext() == null) return false;
Display display = ((WindowManager)MyApplication.getSharedContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
try {
Point screenSize = new Point();
display.getSize(screenSize); // Only available on API 13+
screenW = screenSize.x;
screenH = screenSize.y;
} catch (NoSuchMethodError e) { // The backup methods will only be used if the device is running pre-13, so it's fine that they were deprecated in API 13, thus the suppress warnings annotation at the start of the method.
screenW = display.getWidth();
screenH = display.getHeight();
}
}
// Check if view is outside right and bottom
if (coordinates[0] > screenW) return false;
if (coordinates[1] > screenH) return false;
// Else, view is (at least partially) in the screen bounds
return true;
}
To use it, just pass in any view or subclass of view (IE, just about anything that draws on screen in Android.) It'll return true if it's on screen or false if it's not... pretty intuitive, I think.
If you're not using the above method as a static, then you can probably get a context some other way, but in order to get the Application context from a static method, you need to do these two things:
1 - Add the following attribute to your application tag in your manifest:
android:name="com.package.MyApplication"
2 - Add in a class that extends Application, like so:
public class MyApplication extends Application {
// MyApplication exists solely to provide a context accessible from static methods.
private static Context context;
#Override public void onCreate() {
super.onCreate();
MyApplication.context = getApplicationContext();
}
public static Context getSharedContext() {
return MyApplication.context;
}
}
In addition to the view.getVisibility() there is view.isShown().
isShown checks the view tree to determine if all ancestors are also visible.
Although, this doesn't handle obstructed views, only views that are hidden or gone in either themselves or one of its parents.
In dealing with a similar issue, where I needed to know if the view has some other window on top of it, I used this in my custom View:
#Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if (!hasWindowFocus) {
} else {
}
}
This can be checked using getGlobalVisibleRect method. If rectangle returned by this method has exactly the same size as View has, then current View is completely visible on the Screen.
/**
* Returns whether this View is completely visible on the screen
*
* #param view view to check
* #return True if this view is completely visible on the screen, or false otherwise.
*/
public static boolean onScreen(#NonNull View view) {
Rect visibleRect = new Rect();
view.getGlobalVisibleRect(visibleRect);
return visibleRect.height() == view.getHeight() && visibleRect.width() == view.getWidth();
}
If you need to calculate visibility percentage you can do it using square calculation:
float visiblePercentage = (visibleRect.height() * visibleRect.width()) / (float)(view.getHeight() * view.getWidth())
This solution takes into account view obstructed by statusbar and toolbar, also as view outside the window (e.g. scrolled out of screen)
/**
* Test, if given {#code view} is FULLY visible in window. Takes into accout window decorations
* (statusbar and toolbar)
*
* #param view
* #return true, only if the WHOLE view is visible in window
*/
public static boolean isViewFullyVisible(View view) {
if (view == null || !view.isShown())
return false;
//windowRect - will hold available area where content remain visible to users
//Takes into account screen decorations (e.g. statusbar)
Rect windowRect = new Rect();
view.getWindowVisibleDisplayFrame(windowRect);
//if there is toolBar, get his height
int actionBarHeight = 0;
Context context = view.getContext();
if (context instanceof AppCompatActivity && ((AppCompatActivity) context).getSupportActionBar() != null)
actionBarHeight = ((AppCompatActivity) context).getSupportActionBar().getHeight();
else if (context instanceof Activity && ((Activity) context).getActionBar() != null)
actionBarHeight = ((Activity) context).getActionBar().getHeight();
//windowAvailableRect - takes into account toolbar height and statusbar height
Rect windowAvailableRect = new Rect(windowRect.left, windowRect.top + actionBarHeight, windowRect.right, windowRect.bottom);
//viewRect - holds position of the view in window
//(methods as getGlobalVisibleRect, getHitRect, getDrawingRect can return different result,
// when partialy visible)
Rect viewRect;
final int[] viewsLocationInWindow = new int[2];
view.getLocationInWindow(viewsLocationInWindow);
int viewLeft = viewsLocationInWindow[0];
int viewTop = viewsLocationInWindow[1];
int viewRight = viewLeft + view.getWidth();
int viewBottom = viewTop + view.getHeight();
viewRect = new Rect(viewLeft, viewTop, viewRight, viewBottom);
//return true, only if the WHOLE view is visible in window
return windowAvailableRect.contains(viewRect);
}
you can add to your CustomView's constractor a an onScrollChangedListener from ViewTreeObserver
so if your View is scrolled of screen you can call view.getLocalVisibleRect() and determine if your view is partly offscreen ...
you can take a look to the code of my library : PercentVisibleLayout
Hope it helps!
in your custom view, set the listeners:
getViewTreeObserver().addOnScrollChangedListener(this);
getViewTreeObserver().addOnGlobalLayoutListener(this);
I am using this code to animate a view once when it is visible to user.
2 cases should be considered.
Your view is not in the screen. But it will be visible if user scrolled it
public void onScrollChanged() {
final int i[] = new int[2];
this.getLocationOnScreen(i);
if (i[1] <= mScreenHeight - 50) {
this.post(new Runnable() {
#Override
public void run() {
Log.d("ITEM", "animate");
//animate once
showValues();
}
});
getViewTreeObserver().removeOnScrollChangedListener(this);
getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
}
Your view is initially in screen.(Not in somewhere else invisible to user in scrollview, it is in initially on screen and visible to user)
public void onGlobalLayout() {
final int i[] = new int[2];
this.getLocationOnScreen(i);
if (i[1] <= mScreenHeight) {
this.post(new Runnable() {
#Override
public void run() {
Log.d("ITEM", "animate");
//animate once
showValues();
}
});
getViewTreeObserver().removeOnGlobalLayoutListener(this);
getViewTreeObserver().removeOnScrollChangedListener(this);
}
}