I've put a WebView loading an image inside a ViewPager. When I try to scroll the image horizontally I move over to the next view instead of scrolling the image.
Is it possible to make it scroll to the end of the image before moving over to the next view?
#Override
public Object instantiateItem(View view, int i) {
WebView webview = new WebView(view.getContext());
webview.setHorizontalScrollBarEnabled(true);
webview.loadUrl("http://www.site.with.an/image.gif");
((ViewPager) view).addView(webview, 0);
return webview;
}
The following is a real working solution which will scroll the WebView on a horizontal swipe as long as it can scroll. If the WebView cannot further scroll, the next horizontal swipe will be consumed by the ViewPager to switch the page.
Extending the WebView
With API-Level 14 (ICS) the View method canScrollHorizontally() has been introduced, which we need to solve the problem. If you develop only for ICS or above you can directly use this method and skip to the next section. Otherwise we need to implement this method on our own, to make the solution work also on pre-ICS.
To do so simply derive your own class from WebView:
public class ExtendedWebView extends WebView {
public ExtendedWebView(Context context) {
super(context);
}
public ExtendedWebView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public boolean canScrollHor(int direction) {
final int offset = computeHorizontalScrollOffset();
final int range = computeHorizontalScrollRange() - computeHorizontalScrollExtent();
if (range == 0) return false;
if (direction < 0) {
return offset > 0;
} else {
return offset < range - 1;
}
}
}
Important: Remember to reference your ExtendedWebView inside your layout file instead of the standard WebView.
Extending the ViewPager
Now you need to extend the ViewPager to handle horizontal swipes correctly. This needs to be done in any case -- no matter whether you are using ICS or not:
public class WebViewPager extends ViewPager {
public WebViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
if (v instanceof ExtendedWebView) {
return ((ExtendedWebView) v).canScrollHor(-dx);
} else {
return super.canScroll(v, checkV, dx, x, y);
}
}
}
Important: Remember to reference your WebViewPager inside your layout file instead of the standard ViewPager.
That's it!
Update 2012/07/08: I've recently noticed that the stuff shown above seems to be no longer required when using the "current" implementation of the ViewPager. The "current" implementation seems to check the sub views correctly before capturing the scroll event on it's own (see canScroll method of ViewPager here). Don't know exactly, when the implementation has been changed to handle this correctly -- I still need the code above on Android Gingerbread (2.3.x) and before.
Although Sven mentioned for layout file I want to add detail. After you extend Webview and ViewPager classes,
Inside your activity you will cast to your extended class like this:
web = (MyWebView) findViewById(R.id.webview);
Inside your layout file like this:
<your.package.name.MyWebView
android:id="#+id/webview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
Related
I am using StickyHeaderListview in my project to display contents and for refreshing the list, I am using SwipeRefreshLayout.
The problem here is, when I try to scroll up the list, it starts refreshing the list and not allowing to view the previous items of list.
I want the behavior should be such as the list get refresh only when I've reached to the first item and I try to scroll up , not everytime when i scroll up the list.
Can anyone help on this?
P.s. For implementing SwipeRefreshLayout, I am refering this example
I faced the same problem when using StickyHeaderListview as a direct child of SwipeRefreshLayout. StickyHeaderListview is in fact a FrameLayout wrapping a ListView inside. As nitesh goel explained, this would lead to problems with canChildScrollUp(). Based on nitesh goel's example, this is a full version of CustomSwipeRefreshLayout that works well for me:
public class CustomSwipeRefreshLayout extends SwipeRefreshLayout {
/**
* A StickyListHeadersListView whose parent view is this SwipeRefreshLayout
*/
private StickyListHeadersListView mStickyListHeadersListView;
public CustomSwipeRefreshLayout(Context context) {
super(context);
}
public CustomSwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setStickyListHeadersListView(StickyListHeadersListView stickyListHeadersListView) {
mStickyListHeadersListView = stickyListHeadersListView;
}
#Override
public boolean canChildScrollUp() {
if (mStickyListHeadersListView != null) {
// In order to scroll a StickyListHeadersListView up:
// Firstly, the wrapped ListView must have at least one item
return (mStickyListHeadersListView.getListChildCount() > 0) &&
// And then, the first visible item must not be the first item
((mStickyListHeadersListView.getFirstVisiblePosition() > 0) ||
// If the first visible item is the first item,
// (we've reached the first item)
// make sure that its top must not cross over the padding top of the wrapped ListView
(mStickyListHeadersListView.getListChildAt(0).getTop() < 0));
// If the wrapped ListView is empty or,
// the first item is located below the padding top of the wrapped ListView,
// we can allow performing refreshing now
} else {
// Fall back to default implementation
return super.canChildScrollUp();
}
}
}
Ok I have got it working. If the SwipeRefreshLayout is the root of the layout and the ListView resides deep into the hierarchy (I had put the ListView inside a RelativeLayout along with the empty TextView) and not the direct child of the SwipeRefreshLayout, it won’t detect a swipe up on the list view properly.
You should create a custom class that extends SwipeRefreshLayout and override canChildScrollUp() method in SwipRefreshLayout
Here is a example :
public class CustomSwipeRefreshLayout extends SwipeRefreshLayout{
private AbsListView view;
public CustomSwipeRefreshLayout(Context context) {
super(context);
}
public CustomSwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setView(AbsListView view){
this.view=view;
}
#Override
public boolean canChildScrollUp() {
return view.getFirstVisiblePosition()!=0;
}
}
I have had a similar problem, the direct child should be an instance of ScrollView (or ListView). The SwipeRefreshLayout will only take in account the direct child's scroll and not the child's of that direct child. I managed to solve this by using two SwipeRefreshLayouts.
I posted the code on github.
Hi i think i made something for a generally use :
public class CustomSwipeRefreshLayout extends SwipeRefreshLayout {
private View v;
public CustomSwipeRefreshLayout(Context context) {
super(context);
}
public CustomSwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setView(View v) {
this.v = v;
}
#Override
public boolean canChildScrollUp() {
return this.v.canScrollVertically(-1);
}
}
With that solution, only set the view you want to scroll inside the SwipeRefreshLayout, after call canChildScrollUp(). like this :
this.refreshLayout.setView(aView);
this.refreshLayout.canChildScrollUp();
I don't test it a lot, but if i'm right it will work for every view at every place (direct child or not) in the SwipeRefreshLayout.
(for me it was SwipeRefreshLayout => RelativeLayout => SrcollView => linearLayout)
This is very simple solution:
list.setOnScrollListener(new AbsListView.OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
int topRowVerticalPosition = (list == null || list.getChildCount() == 0) ?
0 : list.getChildAt(0).getTop();
swipeRefreshLayout.setEnabled((topRowVerticalPosition >= 0));
}
});
So, if you're on the top of the listview you will be enabled to do refresh.
When using the translucent status and navigation bars from the new Android 4.4 KitKat APIs, setting fitsSystemWindows="true" and clipToPadding="false" to a ListView works initially. fitsSystemWindows="true" keeps the list under the action bar and above the navigation bar, clipToPadding="false" allows the list to scroll under the transparent navigation bar and makes the last item in the list scroll up just far enough to pass the navigation bar.
However, when you replace the content with another Fragment through a FragmentTransaction the effect of fitsSystemWindows goes away and the fragment goes under the action bar and navigation bar.
I have a codebase of demo source code here along with a downloadable APK as an example: https://github.com/afollestad/kitkat-transparency-demo. To see what I'm talking about, open the demo app from a device running KitKat, tap an item in the list (which will open another activity), and tap an item in the new activity that opens. The fragment that replaces the content goes under the action bar and clipToPadding doesn't work correctly (the navigation bar covers the last item in the list when you scroll all the way down).
Any ideas? Any clarification needed? I posted the before and after screenshots of my personal app being developed for my employer.
I struggled with the same problem yesterday. After thinking a lot, I found an elegant solution to this problem.
First, I saw the method requestFitSystemWindows() on ViewParent and I tried to call it in the fragment's onActivityCreated() (after the Fragment is attached to the view hierarchy) but sadly it had no effect. I would like to see a concrete example of how to use that method.
Then I found a neat workaround: I created a custom FitsSystemWindowsFrameLayout that I use as a fragment container in my layouts, as a drop-in replacement for a classic FrameLayout. What it does is memorizing the window insets when fitSystemWindows() is called by the system, then it propagates the call again to its child layout (the fragment layout) as soon as the fragment is added/attached.
Here's the full code:
public class FitsSystemWindowsFrameLayout extends FrameLayout {
private Rect windowInsets = new Rect();
private Rect tempInsets = new Rect();
public FitsSystemWindowsFrameLayout(Context context) {
super(context);
}
public FitsSystemWindowsFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FitsSystemWindowsFrameLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected boolean fitSystemWindows(Rect insets) {
windowInsets.set(insets);
super.fitSystemWindows(insets);
return false;
}
#Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
super.addView(child, index, params);
tempInsets.set(windowInsets);
super.fitSystemWindows(tempInsets);
}
}
I think this is much simpler and more robust than hacks that try to determine the UI elements sizes by accessing hidden system properties which may vary over time and then manually apply padding to the elements.
I solved the issue by using the library I use the set the color of my translucent status bar.
The SystemBarConfig class of SystemBarTint (as seen here https://github.com/jgilfelt/SystemBarTint#systembarconfig) lets you get insets which I set as the padding to the list in every fragment, along with the use of clipToPadding="false" on the list.
I have details of what I've done on this post: http://mindofaandroiddev.wordpress.com/2013/12/28/making-the-status-bar-and-navigation-bar-transparent-with-a-listview-on-android-4-4-kitkat/
Okay, so this is incredibly weird. I just recently ran into this same issue except mine involves soft keyboard. It initially works but if I add fragment transaction, the android:fitsSystemWindows="true" no longer works. I tried all the solution here, none of them worked for me.
Here is my problem:
Instead of re-sizing my view, it pushes up my view and that is the problem.
However, I was lucky and accidentally stumbled into an answer that worked for me!
So here it is:
First of all, my app theme is: Theme.AppCompat.Light.NoActionBar (if that is relevant, maybe it is, android is weird).
Maurycy pointed something very interesting here, so I wanted to test what he said was true or not. What he said was true in my case as well...UNLESS you add this attribute to your activity in the android manifest of your app:
Once you add:
android:windowSoftInputMode="adjustResize"
to your activity, android:fitsSystemWindows="true" is no longer ignored after the fragment transaction!
However, I prefer you calling android:fitsSystemWindows="true" NOT on the root layout of your Fragment. One of the biggest places where this problem will occur is where if you have EditText or a ListView. If you are stuck in this predicament like I did, set android:fitsSystemWindows="true" in the child of the root layout like this:
YES, this solution works on all Lollipop and pre-lollipop devices.
And here is the proof:
It re-sizes instead of pushing the layout upwards.
So hopefully, I have helped someone who is on the same boat as me.
Thank you all very much!
A heads up for some people running into this problem.
A key piece of information with fitSystemWindows method which does a lot of the work:
This function's traversal down the hierarchy is depth-first. The same
content insets object is propagated down the hierarchy, so any changes
made to it will be seen by all following views (including potentially
ones above in the hierarchy since this is a depth-first traversal).
The first view that returns true will abort the entire traversal.
So if you have any other fragments with content views which have fitsSystemWindows set to true the flag will potentially be ignored. I would consider making your fragment container contain the fitsSystemWindows flag if possible. Otherwise manually add padding.
I've been struggling quite a bit with this as well.
I've seen all the responses here. Unfortunately none of them was fixing my problem 100% of the time.
The SystemBarConfig is not working always since it fails to detect the bar on some devices.
I gave a look at the source code and found where the insets are stored inside the window.
Rect insets = new Rect();
Window window = getActivity().getWindow();
try {
Class clazz = Class.forName("com.android.internal.policy.impl.PhoneWindow");
Field field = clazz.getDeclaredField("mDecor");
field.setAccessible(true);
Object decorView = field.get(window);
Field insetsField = decorView.getClass().getDeclaredField("mFrameOffsets");
insetsField.setAccessible(true);
insets = (Rect) insetsField.get(decorView);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
This is how to get them.
Apparently in Android L there'll be a nice method to get those insets but in the meantime this might be a good solution.
I encountered the same problem. When I replace Fragment.
The 'fitsSystemWindows' doesn't work.
I fixed by code add to your fragment
#Override
public void onViewCreated(final View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
AndroidUtil.runOnUIThread(new Runnable() {
#Override
public void run() {
((ViewGroup) getView().getParent()).setFitsSystemWindows(true);
}
});
}
Combined with #BladeCoder answer i've created FittedFrameLayout class which does two things:
it doesn't add padding for itself
it scan through all views inside its container and add padding for them, but stops on the lowest layer (if fitssystemwindows flag is found it won't scan child deeper, but still on same depth or below).
public class FittedFrameLayout extends FrameLayout {
private Rect insets = new Rect();
public FittedFrameLayout(Context context) {
super(context);
}
public FittedFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FittedFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
public FittedFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
protected void setChildPadding(View view, Rect insets){
if(!(view instanceof ViewGroup))
return;
ViewGroup parent = (ViewGroup) view;
if (parent instanceof FittedFrameLayout)
((FittedFrameLayout)parent).fitSystemWindows(insets);
else{
if( ViewCompat.getFitsSystemWindows(parent))
parent.setPadding(insets.left,insets.top,insets.right,insets.bottom);
else{
for (int i = 0, z = parent.getChildCount(); i < z; i++)
setChildPadding(parent.getChildAt(i), insets);
}
}
}
#Override
protected boolean fitSystemWindows(Rect insets) {
this.insets = insets;
for (int i = 0, z = getChildCount(); i < z; i++)
setChildPadding(getChildAt(i), insets);
return true;
}
#Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
super.addView(child, index, params);
setChildPadding(child, insets);
}
}
I have resolve this question in 4.4
if(test){
Log.d(TAG, "fit true ");
relativeLayout.setFitsSystemWindows(true);
relativeLayout.requestFitSystemWindows();
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}else {
Log.d(TAG, "fit false");
relativeLayout.setFitsSystemWindows(false);
relativeLayout.requestFitSystemWindows();
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
I have 2 webviews on one layout.
I want that on touch, the touched view will get bigger (wider) on the expense of the other view, how can I do it?
Thanks!
Subclass WebView:
public class MyWebView extends WebView{
private MyActivity mActivity;
//Call this on each webview in activity's onCreate after
//instantiating the web views. i.e. after setContentView()
//and assigning it to a variable with findViewById() or
//after programmatically creating the web view.
public setActivity(MyActivity activity){
mActivity = activity;
}
#Override
public boolean onTouchEvent(MotionEvent event){
if (event.getAction()==ACTION_POINTER_DOWN && mActivity!=null){
mActivity.onWebviewTouched(self);
}
super.onTouchEvent(event);
}
}
And in your Activity:
public void onWebviewTouched(MyWebView webView) {
if (webView == mWebviewLeft){
//grow left webview if it isn't already grown, and shrink right webview.
} else if (webView == mWebviewRight) {
//grow right webview if it isn't already grown, and shrink left webview.
}
}
Use the Animation class and its subclasses to modify width of the views smoothly. If using a LinearLayout, you could set one view's layout weight to a constant value, and simply animate the layout weight of the other view to get bigger or smaller than that constant value.
If you just want an instant change, just set the view widths directly with LayoutParams.
I have the following structure of my app:
v4 FragmentActivity --> v4 ViewPager --> v4 Fragment
--> v4 ListFragment
I'm using ActionBarSherlock (which I would really recommend), and the structure of the Activity is based on the demo at https://github.com/JakeWharton/ActionBarSherlock/blob/master/samples/demos/src/com/actionbarsherlock/sample/demos/app/ActionBarTabsPager.java . So the two fragments are displayed as two tabs for the user.
When a user clicks an element of the ListFragment I want to load an url in a WebView in the same place as the list is. That is, I want to replace the ListFragment (put it on the back stack) with a new WebView.
So far I've tried using FragmentTransaction.replace() from the Activity. That kind of works, except two issues:
The ListFragment doesn't display the WebView before I rotate the device (i.e. the acitity is recreated).
The content of the other tab disappears (it's just blank)
What is the correct way to replace the ListFragment with another Fragment?
Alternatevely, you could use a custom HorizontalScrollView instead of a ViewPager and overwrite its onTouchEvent method to get the same snapping effect you get with a ViewPager, like this:
public class MyHorizontalScrollView extends HorizontalScrollView {
public MyHorizontalScrollView(Context context) {
super(context);
}
public MyHorizontalScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyHorizontalScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_UP:
if (mScrollable) {
View child = getChildAt(0);
if (child != null) {
final int currentX = getScrollX();
final int windowWidth = getResources().getDisplayMetrics().widthPixels;
final int totalWidth = child.getWidth();
int showingElementNumber = 1;
int i = windowWidth;
while (i < currentX && i < totalWidth) {
i+=windowWidth;
showingElementNumber++;
}
int scrollTo = 0;
if (currentX < (windowWidth * (showingElementNumber - 1) + windowWidth/2)) { // Previouses widths + half the current
scrollTo = windowWidth * (showingElementNumber - 1);
} else {
scrollTo = windowWidth * showingElementNumber + marginSize;
}
smoothScrollTo(scrollTo, 0);
return false;
}
}
return super.onTouchEvent(ev);
default:
return super.onTouchEvent(ev);
}
}
}
Then you could add two Fragments inside it and use a regular FragmentTransaction with addToBackStack() to get what you want.
NOTE: If you want to use the above code, just make sure your Fragments are the same width as the entire screen and remember you structure should be something like
MyHorizontalScrollView > LinearLayout > YourFragment1
> YourFragment2
As a workaround I've done what AndroidTeam At Mail.Ru and connoisseur suggest as answers on Replace Fragment inside a ViewPager. This is of course a pretty bad way solving this. I feel kinda dirty now. :-S
how can I set scroll of scrollview to x pixels, before it's even shown?
In ScrollView I have some Views and i know that it will fill screen. Can I scroll to let say second View, so that first View is not visible when activity is started?
Now I have sth like this, but I think it's not perfect, sometimes, I see first View, and then it's scrolled to the second one
#Override
public void onGlobalLayout() {
if (mHorizontalScrollView.getChildCount() > 0 && mHorizontalScrollView.getChildAt(0).getWidth() > mScreenWidth) {
hsv.scrollTo(100, 0);
}
}
EDIT!!
Second attempt was to use http://developer.android.com/reference/android/view/ViewTreeObserver.OnPreDrawListener.html instead of http://developer.android.com/reference/android/view/ViewTreeObserver.OnGlobalLayoutListener.html
In OnPreDrawListener we can read that At this point, all views in the tree have been measured and given a frame. Clients can use this to adjust their scroll bounds or even to request a new layout before drawing occurs. so basically at this point we should adjust scrolling. So I created:
#Override
public boolean onPreDraw() {
if (hsv.getChildCount() > 0 && hsv.getChildAt(0).getWidth() > mScreenWidth) {
hsv.scrollTo(100, 0);
return false;
}
return true;
}
but now it's never working for me.
I combined onGlobalLayout with code below. It's not so great to use that hack but it still quite efficient. When I get onGlobalLayout event I check if layouts are done and then if it's ok I use method below.
// hsv - horizontal scroll view
private void forceShowColumn(final int column) {
int scrollTo = childViewWidth * column;
if (scrollTo == hsv.getScrollX()) {
return;
}
hsv.scrollTo(scrollTo, 0);
hsv.postDelayed(new Runnable() {
#Override
public void run() {
forceShowColumn(column);
}
}, 10);
}
try View.post(Runnable) in onCreate-method.
I solved this issue by implementing View.OnLayoutChangeListener in the fragment that controls my ScrollView.
public class YourFragment extends Fragment implements View.OnLayoutChangeListener{
...
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
view = inflater.inflate(R.layout.your_fragment_layout, container, false);
//set scrollview layout change listener to be handled in this fragment
sv = (ScrollView) view.findViewById(R.id.your_scroll_view);
sv.addOnLayoutChangeListener(this);
return view;
}
...
public void onLayoutChange (View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom){
switch (v.getId())
{
case R.id.your_scroll_view:
{
sv.scrollTo(0, sv.getTop());
}
}
}
The correct way to do this, is to call scrollTo() on the ScrollView after the view has been through its measurement and layout passes, otherwise the width of the content in the scrollView will be 0, and thus scrollTo() will not scroll to the right position. Usually after the constructor has been called and after the view has been added to its parent.
See http://developer.android.com/guide/topics/ui/how-android-draws.html for more insight on how android draws views.
MrJre gives the most elegant solution: "override onLayout() in the scrollview and do it there after super.onLayout()"
Example code here:
https://stackoverflow.com/a/10209457/1310343
Use :
scrollTo (int x, int y)
It sets the scrolled position of your view. This will cause a call to onScrollChanged
For more details : see this
ScrollView ScrollTo