I am having a problem with a scrollView inside a fragment. My tablet application contain two main fragments one for the menu on the left and a other one on the right which contain an editText object and some other stuff.
I am trying to scroll vertically the content of the right fragment when the soft keyboard is showing.
The content scroll in the right fragment is working but after scroll the fragment seem to be cropped on the top and bottom where the system bars appeared while the soft keyboard was showing.
Layout before and after scroll.
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.wenchao.cardstack.CardStack
android:id="#+id/card_stack_comment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="20dp"
android:visibility="visible"
android:background="#android:color/transparent"
android:clipChildren="false"
android:clipToPadding="false">
</com.wenchao.cardstack.CardStack>
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="200dp"
android:orientation="vertical"
android:layout_alignParentBottom="true"
android:id="#+id/general_comment">
<EditText
android:id="#+id/editText_feedback"
android:layout_below="#+id/text_leave_feedback"
android:layout_margin="10dp"
android:hint="#string/editText_hint"
android:padding="10dp"
android:gravity="top"
android:background="#drawable/background_with_border"
android:layout_width="match_parent"
android:layout_height="200dp" />
</RelativeLayout>
</RelativeLayout>
</ScrollView>
This is how I detect if the keyboard is showing and move the content of the
ScrollView:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
rootView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
Rect r = new Rect();
// r will be populated with the coordinates of your view
// that area still visible.
rootView.getWindowVisibleDisplayFrame(r);
int heightDiff = rootView.getRootView().getHeight() - (r.bottom - r.top);
if (heightDiff > 300) { // if more than 100 pixels, its probably a keyboard...
Log.d(LOG_TAG, "keyboard shows up");
scrollToCurrentFocusedView();
}else{
Log.d(LOG_TAG, "keyboard vanishes");
//try to refresh the scrollView but not working
//scrollView.invalidate()
//scrollView.requestLayout()
}
}
});
return rootView;
}
protected void scrollToCurrentFocusedView(){
Log.d(LOG_TAG, "calling scrollToCurrentFocusedView");
View view = getActivity().getCurrentFocus();
if (view != null && scrollView != null) {
scrollView.smoothScrollTo(0, view.getBottom()-(view.getHeight()*3));
}
}
I tried to refresh the scrollview after the keyboard is hidden with invalidate() and requestLayout() without success.
Any help would be appreciated. Thanks in advance.
Fixed the problem by using a customScrollView instead. The important part is to overload the onApplyWindowInsets() function.
public class CustomScrollView extends ScrollView {
public CustomScrollView(Context pContext, AttributeSet pAttrs, int pDefStyle) {
super(pContext, pAttrs, pDefStyle);
}
public CustomScrollView(Context pContext, AttributeSet pAttrs) {
super(pContext, pAttrs);
}
public CustomScrollView(Context pContext) {
super(pContext);
}
#Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
if(insets.getSystemWindowInsetBottom() < 100){
WindowInsets newInset = insets.replaceSystemWindowInsets(new Rect(0,0,0,0));
Log.d("TEST","Keyboard is down");
return super.onApplyWindowInsets(newInset);
} else{
Log.d("TEST","Keyboard is up");
return super.onApplyWindowInsets(insets);
}
}
}
Related
Using BottomSheetBehavior from the google design library, it looks like the default behavior is for the bottom sheet to "cover" other views in the same CoordinatorLayout as it expands. I can anchor something like a FAB (or other view with an appropriately defined CoordinatorLayout.Behavior) to the top of the sheet and have it slide up as the sheet expands, which is nice, but what I want is to have a view "collapse" as the bottom sheet expands, showing a parallax effect.
This effect in Google Maps is similar to what I'm looking for; it starts as a parallax effect, and then switches back to just having the bottom sheet "cover" the map once a certain scroll position is reached:
One thing I tried (though I suspected from the start it wouldn't work), was setting the upper view's height programmatically in the onSlide call of my BottomSheetBehavior.BottomSheetCallback. This was somewhat successful, but the movement wasn't nearly as smooth as in Google Maps.
If anyone has an idea how the effect is accomplished I would appreciate it a lot!
After a bit more experimenting/research I realized from this post
How to make custom CoordinatorLayout.Behavior with parallax scrolling effect for google MapView? that a big part of my problem was not understanding the parallax effect, which translates views rather than shrinking them. Once I realized that, it was trivial to create a custom behavior that would apply the parallax to my main view when the bottom sheet expanded:
public class CollapseBehavior<V extends ViewGroup> extends CoordinatorLayout.Behavior<V>{
public CollapseBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
if (isBottomSheet(dependency)) {
BottomSheetBehavior behavior = ((BottomSheetBehavior) ((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior());
int peekHeight = behavior.getPeekHeight();
// The default peek height is -1, which
// gets resolved to a 16:9 ratio with the parent
final int actualPeek = peekHeight >= 0 ? peekHeight : (int) (((parent.getHeight() * 1.0) / (16.0)) * 9.0);
if (dependency.getTop() >= actualPeek) {
// Only perform translations when the
// view is between "hidden" and "collapsed" states
final int dy = dependency.getTop() - parent.getHeight();
ViewCompat.setTranslationY(child, dy/2);
return true;
}
}
return false;
}
private static boolean isBottomSheet(#NonNull View view) {
final ViewGroup.LayoutParams lp = view.getLayoutParams();
if (lp instanceof CoordinatorLayout.LayoutParams) {
return ((CoordinatorLayout.LayoutParams) lp)
.getBehavior() instanceof BottomSheetBehavior;
}
return false;
}
}
Then in my layout XML, I set the app:layout_behavior of my main view to be com.mypackage.CollapseBehavior and the app:layout_anchor to be my bottom sheet view so that the onDependentViewChanged callback would trigger. This effect was much smoother than trying to resize the view. I suspect returning to my initial strategy of using a BottomSheetBehavior.BottomSheetCallback would also work similarly to this solution.
Edit: per request, the relevant XML is below. I add a MapFragment into #+id/map_container at runtime, though this should also work with anything you drop into that container like a static image. The LocationListFragment could likewise be replaced with any view or fragment, so long as it still has the BottomSheetBehavior
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/fragment_coordinator">
<FrameLayout
android:id="#+id/map_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
app:layout_anchor="#+id/list_container"
app:layout_behavior="com.mypackage.behavior.CollapseBehavior"/>
<fragment
android:name="com.mypackage.fragment.LocationListFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/list_container"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior"/>
</android.support.design.widget.CoordinatorLayout>
Patrick Grayson's post was very helpful. In my case though, I did need something that resized the map. I adopted the solution above to resize instead of translate. Perhaps others may be looking for a similar solution.
public class CollapseBehavior<V extends ViewGroup> extends CoordinatorLayout.Behavior<V> {
private int pixels = NO_RESIZE_BUFFER; // default value, in case getting a value from resources, bites the dust.
private static final int NO_RESIZE_BUFFER = 200; //The amount of dp to not have the bottom sheet ever push away.
public CollapseBehavior(Context context, AttributeSet attrs)
{
super(context, attrs);
pixels = (int)convertDpToPixel(NO_RESIZE_BUFFER,context);
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
// child is the map
// dependency is the bottomSheet
if(isBottomSheet(dependency))
{
BottomSheetBehavior behavior = ((BottomSheetBehavior) ((CoordinatorLayout.LayoutParams)dependency.getLayoutParams()).getBehavior());
int peekHeight;
if (behavior != null) {
peekHeight = behavior.getPeekHeight();
}
else
return true;
if(peekHeight > 0) { // Dodge the case where the sheet is hidden.
if (dependency.getTop() >= peekHeight) { // Otherwise we'd completely overlap the map
if(dependency.getTop() >= pixels) { // On resize when we have more than our NO_RESIZE_BUFFER of dp left.
if(dependency.getTop() > 0) { // Don't want to map to be gone completely.
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
params.height = dependency.getTop();
child.setLayoutParams(params);
}
return true;
}
}
}
}
return false;
}
private static float convertDpToPixel(float dp, Context context)
{
float densityDpi = context.getResources().getDisplayMetrics().densityDpi;
return dp * (densityDpi / DisplayMetrics.DENSITY_DEFAULT);
}
private static boolean isBottomSheet(#NonNull View view)
{
final ViewGroup.LayoutParams lp = view.getLayoutParams();
if(lp instanceof CoordinatorLayout.LayoutParams)
{
return ((CoordinatorLayout.LayoutParams) lp).getBehavior() instanceof BottomSheetBehavior;
}
return false;
}
}
And the layout...
<FrameLayout
android:id="#+id/flMap"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="top"
android:layout_margin="0dp"
app:layout_anchor="#+id/persistentBottomSheet"
app:layout_behavior="com.yoursite.yourapp.CollapseBehavior">
<fragment
android:id="#+id/mapDirectionSummary"
android:name="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.yoursite.yourapp.YourActivity" />
</FrameLayout>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/persistentBottomSheet"
app:behavior_peekHeight="#dimen/bottom_sheet_peek_height"
app:behavior_hideable="false"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior"
tools:context="com.yoursite.yourapp.YourActivity">
<!-- Whatever you want on the bottom sheet. -->
</android.support.constraint.ConstraintLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardElevation="8dp"
app:cardBackgroundColor="#324">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="?attr/colorPrimary"
android:theme="#style/ThemeOverlay.AppCompat.ActionBar"
app:popupTheme="#style/Theme.AppCompat.Light">
<EditText
android:id="#+id/txtSearch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#android:color/transparent"
android:ems="10"
android:inputType="text"
android:maxLines="1" />
</android.support.v7.widget.Toolbar>
</android.support.v7.widget.CardView>
</LinearLayout>
I want to try BottomSheetDialog introduced in Android Support Library 23.2 but it doesn't seem to work correctly. Here is what the doc says:
While BottomSheetBehavior captures the persistent bottom sheet case, this release also provides a BottomSheetDialog and
BottomSheetDialogFragment to fill the modal bottom sheets use case.
Simply replace AppCompatDialog or AppCompatDialogFragment with their
bottom sheet equivalents to have your dialog styled as a bottom
sheet."
So I changed my AppCompatDialog to BottomSheetDialog:
package my.package.ui.dialog;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.support.design.widget.BottomSheetDialog;
import my.package.R;
public class AccountActionsDialog extends BottomSheetDialog {
public AccountActionsDialog(Context context) {
super(context);
if (context instanceof Activity) {
setOwnerActivity((Activity) context);
}
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(getLayoutInflater().inflate(R.layout.dialog_account_actions, null));
}
}
Here is my layout file:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#ff0000"
android:padding="16dp"
android:text="Delete account"
android:textColor="#ffffff" />
</LinearLayout>
Then I use the following code in my Activity:
new AccountActionsDialog(this).show();
My screen becomes dimmed but the content of my dialog is not visible. Any thoughts on what might be missing? It works fine when I use AppCompatDialog instead.
Instead of having a separate class, you can simply create an instance for BottomSheetDialog in your Activity/Fragment like following and you can use it. It is very easier and simpler I think.
val dialog = BottomSheetDialog(this)
val bottomSheet = layoutInflater.inflate(R.layout.bottom_sheet, null)
bottomSheet.buttonSubmit.setOnClickListener { dialog.dismiss() }
dialog.setContentView(bottomSheet)
dialog.show()
This is the layout file of BottomSheetDialog.
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:soundEffectsEnabled="false">
<FrameLayout
android:id="#+id/design_bottom_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
app:layout_behavior="#string/bottom_sheet_behavior"
style="?attr/bottomSheetStyle"/>
</android.support.design.widget.CoordinatorLayout>
Your content view is inside the view design_bottom_sheet, it will be positioned center vertically by CoordinatorLayout, and BottomSheetBehavior will offset it.
mParentHeight = parent.getHeight();
mMinOffset = Math.max(0, mParentHeight - child.getHeight());
mMaxOffset = mParentHeight - mPeekHeight;
if (mState == STATE_EXPANDED) {
ViewCompat.offsetTopAndBottom(child, mMinOffset);
} else if (mHideable && mState == STATE_HIDDEN) {
ViewCompat.offsetTopAndBottom(child, mParentHeight);
} else if (mState == STATE_COLLAPSED) {
ViewCompat.offsetTopAndBottom(child, mMaxOffset);
}
It intented to positon design_bottom_sheet at mMaxOffset, but actually the initial getTop of the child view is not 0, but (mParentHeight - childHeight) / 2, so you view if offset more than the desired offset.
Find the view design_bottom_sheet and set its gravity to Gravity.TOP | Gravity.CENTER_HORIZONTAL will fix it. But, if the childHeight is less than mPeekHeight, there will be blank area below you content view.
However, if peekHeight > childHeight, the mMaxOffset will less than mMinOffset, which will cause weird behavior.
Maybe the code should be changed to
mMaxOffset = Math.max((mParentHeight - mPeekHeight), mMinOffset);
insted of
mMaxOffset = mParentHeight - child.getHeight();
Here's the issue on code.google.com https://code.google.com/p/android/issues/detail?id=201793
An issue some users are seeing boils down to the FrameLayout that wraps our content view being centered vertically. The BottomSheetBehavior only works if this view is top aligned. I haven't figured out what causes the FrameLayout to become centered vertically yet, but here's a possible workaround:
View contentView = ...
// You may have to measure your content view first.
dialog.setContentView(contentView);
// Change this to a percentage or a constant, whatever you want to do.
// The default is 1024 - any views smaller than this will be pulled off
// the bottom of the screen.
float peekHeight = contentView.getMeasuredHeight();
View parent = (View)contentView.getParent();
BottomSheetBehavior behavior = BottomSheetBehavior.from(parent);
behavior.setPeekHeight(peekHeight);
CoordinatorLayout.LayoutParams layoutParams =
(CoordinatorLayout.LayoutParams)parent.getLayoutParams();
layoutParams.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
I was expriencing the same issue, dimmed background and content not visible. Here is how I managed to workaround it by setting the content view in setupDialog() hidden method.
public class CustomBottomSheetDialogFragment extends BottomSheetDialogFragment {
private TextView mOffsetText;
private TextView mStateText;
private BottomSheetBehavior.BottomSheetCallback mBottomSheetBehaviorCallback = new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(#NonNull View bottomSheet, int newState) {
setStateText(newState);
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
dismiss();
}
}
#Override
public void onSlide(#NonNull View bottomSheet, float slideOffset) {
setOffsetText(slideOffset);
}
};
private LinearLayoutManager mLinearLayoutManager;
private ApplicationAdapter mAdapter;
#Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return super.onCreateDialog(savedInstanceState);
}
#Override
public void onViewCreated(View contentView, #Nullable Bundle savedInstanceState) {
super.onViewCreated(contentView, savedInstanceState);
}
#Override
public void setupDialog(Dialog dialog, int style) {
super.setupDialog(dialog, style);
View contentView = View.inflate(getContext(), R.layout.bottom_sheet_dialog_content_view, null);
dialog.setContentView(contentView);
mBottomSheetBehavior = BottomSheetBehavior.from(((View) contentView.getParent()));
if (mBottomSheetBehavior != null) {
mBottomSheetBehavior.setBottomSheetCallback(mBottomSheetBehaviorCallback);
}
mOffsetText = (TextView) contentView.findViewById(R.id.offsetText);
mStateText = (TextView) contentView.findViewById(R.id.stateText);
}
}
And the layout:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="#+id/offsetText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#android:color/black" />
<TextView
android:id="#+id/stateText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#android:color/black" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
It started to work when I set fixed height for my TextView (200dp), although for some height values it still behaves incorrectly. Obviously it's an issue of support lib. There are already few reports related to BottomSheetDialog in the bug tracker:
https://code.google.com/p/android/issues/detail?id=201793&sort=-opened&colspec=ID%20Status%20Priority%20Owner%20Summary%20Stars%20Reporter%20Opened
https://code.google.com/p/android/issues/detail?id=201826
I am trying to use a Snackbar. I have a FloatingActionButton wrapped in a CoordinatorLayout. When the Snackbar shows, the button is correctly moved up. When it dismisses automatically, the button moves down. But if I dismiss the Snackbar programmatically, the button does not go down. My code is simple:
mSnackbar = Snackbar.make(mCoordinatorLayout, text, Snackbar.LENGTH_LONG)
.setAction(R.string.undo, new View.OnClickListener() {
#Override
public void onClick(View v) {
undoDeleteTasks();
}
});
mSnackbar.show();
Is there a way to make the FloatingActionButton move down when the Snackbar is dismissed programmatically?
Try this:
Snackbar mysnack = Snackbar.make( fab, "Hi, welcome to my app!", Snackbar.LENGTH_LONG );
mysnack.getView().addOnAttachStateChangeListener( new View.OnAttachStateChangeListener() {
#Override
public void onViewAttachedToWindow( View v ) {
}
#Override
public void onViewDetachedFromWindow( View v ) {
fab.setTranslationY( 0 );
}
});
mysnack.show();
The problem is, in an CoordinatorLayout, the FAB is moved because its behavior class's onDependentViewChanged() is called when the Snackbar animates in or out.
However, when you call Snabackbar.dismiss() no animation takes place. And therefore no onDependentViewChanged(). And thus movement of the FAB.
I want to know if it possible to either
animate the Snackbar dismiss?
create a behavior that moves the FAB when the Snackbar causes onDependentViewRemoved()?
Farzad119's answer is arguably a little brittle. My approach--although not satisfactory either--is to alter the behavior on the FAB to move the FAB down when the SnackBar is removed:
#Override
public void onDependentViewRemoved(CoordinatorLayout parent, FloatingActionButton child, View dependency) {
super.onDependentViewRemoved(parent, child, dependency);
float translationY = Math.min(0, parent.getBottom() - child.getBottom());
child.setTranslationY(translationY);
}
To solve this issue I defined a RelativeLayout Behavior like this. This can be done for any view, just replace all RelativeLayout with desired UI element.
public class RelativeLayoutBehavior extends CoordinatorLayout.Behavior<RelativeLayout> {
public RelativeLayoutBehavior(Context context, AttributeSet attrs) {
}
#Override
public boolean layoutDependsOn(CoordinatorLayout parent, RelativeLayout child, View dependency) {
return dependency instanceof Snackbar.SnackbarLayout;
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, RelativeLayout child, final View dependency) {
Float translationY = Math.min(0, dependency.getTranslationY() - dependency.getHeight());
child.setTranslationY(translationY);
return true;
}
#Override
public void onDependentViewRemoved(CoordinatorLayout parent, RelativeLayout child, View dependency) {
child.setTranslationY(0);
}
}
Then you need to add the Behavior into the RelativeLayout "android:layout_behavior" property like this. Make sure the RelativeLayout you want to slide up is inside of a CoordinateLayout also.
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/main_coordinate_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#323232"
tools:context="com.choch.michaeldicioccio.myapplication.MainActivity">
<RelativeLayout
android:id="#+id/bottom_navigation_relative_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:gravity="bottom"
app:layout_behavior="com.yourPackagePath.RelativeLayoutBehavior" >
<android.support.design.widget.BottomNavigationView
android:id="#+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="#color/colorPrimary"
android:clickable="true"
app:elevation="8dp"
app:itemBackground="#color/colorPrimary"
app:itemIconTint="#drawable/bottom_navigation_color_selector"
app:itemTextColor="#drawable/bottom_navigation_color_selector"
app:menu="#menu/bottom_bar_menu" />
<android.support.design.widget.FloatingActionButton
android:id="#+id/add_car_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_above="#id/bottom_navigation"
android:layout_margin="#dimen/fab_margin"
android:scaleType="center"
android:src="#mipmap/ic_plus_black_36dp"
android:tint="#color/colorPrimary"
app:elevation="8dp"/>
</RelativeLayout>
</android.support.design.widget.CoordinatorLayout>
I need to design a ViewPager which able to pass childs with fixed width (e.g childs with 700dp width), Unfortunately the current version of ViewPager will automatically makes all childrens width to MATCH_PARENT, is there any way to add this functionality to ViewPager?
My ViewPager layout:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<android.support.v4.view.ViewPager
android:id="#+id/some_id"
android:layout_width="match_parent"
android:layout_height="300dp"
android:overScrollMode="never" />
</LinearLayout>
ViewPager childs layout:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/banner_main_layout_container"
android:layout_width="700dp"
android:layout_height="match_parent"
android:background="#fff"
android:gravity="center"
android:orientation="vertical" >
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="some images"/>
</LinearLayout>
Thanks in Advance...
It is possible to scale the pages within the ViewPager with FragmentPagerAdapter.getPageWidth. You will need a custom FragmentPagerAdapter. If you return a number between 0 and 1, the pages are scaled down, width > 1 scales pages up accordingly. But this is not really good, because you can't scroll the image within the up-scaled page.
If you wrap the ImageView in a HorizontalScrollView, things are a bit better, you can scroll the images within pages, but the swipe gesture between pages is caught by the HorizontalScrollView if you are not very fast. See this video.
So the solution is truly to use a custom HorizontalScrollView (see InterceptingHorizontalScrollView) which disallows intercepting the onTouch event, but also allows it when the User scrolls to the end (See overidden onOverScrolled). See this video or the image below for the difference.
EDIT You don't need to override onInterceptTouchEvent, because HorizontalScrollView intercepts them by default (so scrolling the image has higher priority than paging.)
Finally, here's all the code:
MainActivity.java
public class MainActivity extends Activity {
private ViewPager mViewPager;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Set up the ViewPager
mViewPager = (ViewPager) findViewById(R.id.pager);
mViewPager.setPageMargin(30);
mViewPager.setAdapter(new ImagePagerAdapter(getFragmentManager()));
}
private class ImagePagerAdapter extends FragmentPagerAdapter {
public ImagePagerAdapter(FragmentManager fm) {
super(fm);
}
#Override
public Fragment getItem(int i) {
switch(i) {
case 0:
return ImageFragment.newInstance(R.drawable.forest1);
case 1:
return ImageFragment.newInstance(R.drawable.forest2);
case 2:
return ImageFragment.newInstance(R.drawable.forest3);
default:
return ImageFragment.newInstance(R.drawable.ic_launcher);
}
}
#Override
public int getCount() {
return 3;
}
#Override
public float getPageWidth(int position)
{
// Here it is possible to scale page width
return super.getPageWidth(position);
}
}
}
activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<android.support.v4.view.ViewPager
android:id="#+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
ImageFragment.java
public class ImageFragment extends Fragment {
private static final String ARG_PARAM1 = "image_resid";
private int mImageResId;
public static ImageFragment newInstance(int image_resid) {
ImageFragment fragment = new ImageFragment();
Bundle args = new Bundle();
args.putInt(ARG_PARAM1, image_resid);
fragment.setArguments(args);
return fragment;
}
public ImageFragment() {
// Required empty public constructor
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mImageResId = getArguments().getInt(ARG_PARAM1);
}
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_image, container, false);
ImageView imageView = (ImageView)v.findViewById(R.id.imageView);
imageView.setImageResource(mImageResId);
return v;
}
}
fragment_image.xml
<com.gyebro.viewpagertest.InterceptingHorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="600dp"
android:layout_height="match_parent"
tools:context="com.gyebro.viewpagertest.ImageFragment">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:id="#+id/imageView"
android:src="#drawable/forest1" />
</com.gyebro.viewpagertest.InterceptingHorizontalScrollView>
InterceptingHorizontalScrollView.java
public class InterceptingHorizontalScrollView extends HorizontalScrollView {
public InterceptingHorizontalScrollView(Context context) {
super(context);
}
public InterceptingHorizontalScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public InterceptingHorizontalScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/*#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (getParent() != null) {
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
getParent().requestDisallowInterceptTouchEvent(false);
break;
}
}
return super.onInterceptTouchEvent(ev);
}*/
#Override
protected void onOverScrolled (int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
super.onOverScrolled(scrollX,scrollY,clampedX,clampedY);
// if clampedX == true, we've reached the end of the HorizontalScrollView so
// allow parent to intercept
if(clampedX) {
Log.d("InterceptingHorizontalScrollView", "Reached the end, allowing interception");
getParent().requestDisallowInterceptTouchEvent(false);
}
}
}
What you really want here is a HorizontalScrollView inside of a ViewPager. This requires custom touch handling, so you'll want to use something like this class: InterceptingHorizontalScrollView
To make InterceptingHorizontalScrollView work in a ViewPager, you'll have to override onOverScrolled:
#Override
protected void onOverScrolled (int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
super.onOverScrolled(scrollX,scrollY,clampedX,clampedY);
if(clampedX) {
getParent().requestDisallowInterceptTouchEvent(false);
}
}
Thanks to Gyebro for this tip.^
Your ViewPager child layout would look like this:
<com.tumblr.widget.InterceptingHorizontalScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:id="#+id/banner_main_layout_container"
android:layout_width="700dp"
android:layout_height="match_parent"
android:background="#fff"
android:gravity="center"
android:orientation="vertical" >
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="some images"/>
</LinearLayout>
</com.tumblr.widget.InterceptingHorizontalScrollView>
You can either override PagerAdapter´s getWidth method and if this does not help, look at this:
http://commonsware.com/blog/2012/08/20/multiple-view-viewpager-options.html
and most importantly try this example, it works great!
Just go along the whole example.
The ViewPager children will/should always match it's parent width.
Furthermore it sounds like a bad idea to use a 700dp width ImageView. What would that look like in portrait mode?
If you don't want to make the ViewPager itself smaller, i.e. you want the ImageViews to be swiped from the absolute side of the screen, you have to make the items appear smaller.
That imitation could be done by creating 2 additional LinearLayouts to act as spacers. Then it would appear as if your item has a specific width.
Here's an example (with a TextView):
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<LinearLayout
android:background="#FFF"
android:layout_weight="3"
android:layout_width="0dp"
android:layout_height="match_parent"/>
<TextView
android:layout_weight="10"
android:background="#333"
android:textColor="#FFF"
android:textSize="50sp"
android:gravity="center"
android:text="HELLO"
android:layout_width="0dp"
android:layout_height="match_parent" />
<LinearLayout
android:background="#FFF"
android:layout_weight="3"
android:layout_width="0dp"
android:layout_height="match_parent"/>
</LinearLayout>
And that would look like this:
Set the page margin of the view pager to a negative value. This will force the pages to push into the the view pager. Be warned, it will also cause overlap so you'll see part of the next/previous element in the view pager.
i am using SwipeRefreshLayout in my below layout:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#color/homePageBackground"
android:orientation="vertical" >
<android.support.v4.widget.SwipeRefreshLayout
android:id="#+id/swipeRefreshLayout_listView"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<fragment
android:id="#+id/announcementHomefragment"
android:name="in.test.app.AnnouncementFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/homePageBackground" >
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="15dp"
android:background="#color/homePageBackground" >
<TextView
android:id="#+id/newsTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginLeft="15dp"
android:layout_marginTop="5dp"
android:gravity="center"
android:text="#string/new_list"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="#color/white"
android:textStyle="bold" />
<fragment
android:id="#+id/newshomefragment"
android:name="in.test.app.NewsFragment"
android:layout_width="wrap_content"
android:layout_height="190dp"
android:layout_below="#id/newsTitle"
android:layout_marginTop="-15dp" />
<TextView
android:id="#+id/productTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#id/newshomefragment"
android:layout_gravity="center"
android:layout_marginLeft="15dp"
android:layout_marginTop="5dp"
android:gravity="center"
android:text="#string/product_in_home"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="#color/white"
android:textStyle="bold" />
<fragment
android:id="#+id/proCategoryhomefragment"
android:name="in.test.app.CategoryFragment"
android:layout_width="wrap_content"
android:layout_height="170dp"
android:layout_below="#id/productTitle"
android:layout_marginTop="-15dp" />
<TextView
android:id="#+id/trainingTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#id/proCategoryhomefragment"
android:layout_gravity="center"
android:layout_marginLeft="15dp"
android:layout_marginTop="2dp"
android:gravity="center"
android:text="#string/trainings_in_home"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="#color/white"
android:textStyle="bold" />
<fragment
android:id="#+id/trainingfragment"
android:name="in.test.app.TrainingFragment"
android:layout_width="match_parent"
android:layout_height="180dp"
android:layout_below="#id/trainingTitle"
android:layout_marginBottom="10dp"
android:layout_marginTop="-15dp" />
</RelativeLayout>
</ScrollView>
</LinearLayout>
</android.support.v4.widget.SwipeRefreshLayout>
When I pull down my SwipeRefreshLayout it is working, but as you can see in the above code I have a scroll view inside that. So when I am pulling down my scroll view, it goes down and half the images are not showing because it came down. When I am trying to pull up again my scroll view is not going up. Instead, SwipeRefreshLayout is getting call. What should i do?
Please help me out.
I would say it's better to have an extended SwipeRefreshLayout with listener to be able to add various conditions from the classes that display this layout.
Something like the following:
GeneralSwipeRefreshLayout.java
public class GeneralSwipeRefreshLayout extends SwipeRefreshLayout {
private OnChildScrollUpListener mScrollListenerNeeded;
public interface OnChildScrollUpListener {
boolean canChildScrollUp();
}
public GeneralSwipeRefreshLayout(Context context) {
super(context);
}
public GeneralSwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* Listener that controls if scrolling up is allowed to child views or not
*/
public void setOnChildScrollUpListener(OnChildScrollUpListener listener) {
mScrollListenerNeeded = listener;
}
#Override
public boolean canChildScrollUp() {
if (mScrollListenerNeeded == null) {
Log.e(GeneralSwipeRefreshLayout.class.getSimpleName(), "listener is not defined!");
}
return mScrollListenerNeeded != null && mScrollListenerNeeded.canChildScrollUp();
}
}
And then inside your class that displays SwipeRefreshLayout containing ListView or GridView layout, you can do something like this:
mSwipeLayout.setOnChildScrollUpListener(new OnChildScrollUpListener() {
#Override
public boolean canChildScrollUp() {
return mListView.getFirstVisiblePosition() > 0 ||
mListView.getChildAt(0) == null ||
mListView.getChildAt(0).getTop() < 0;
}
});
Just create a class which extends SwipeRefreshLayout and override the method canChildScrollUp(). Return true when you want scroll down for your control.
For example for scrollview you may try this,
#override.
boolean canChildScrollUp()
{
//your condition to check scrollview reached at top while scrolling
if(scrollview.getScrollY() == 0.0)
return true;
else
return false;
}
As others have already stated, if you don't have your scrollable view (ie listview) as the direct child of the SwipeRefreshLayout, the stock canChildScrollUp will not work.
Has to do with the simple logic SwipeRefreshLayout uses in checking the ability of the child view to scroll.
I was using a ListView inside an ActionbarActivity, and wanted to include an empty view whenever my listview was empty. This caused problems, since the SwipeRefreshLayout class can only have a single child. Note it also checks this child's ability to scrollUp to determine if a pull down causes the child to scrollUp, or if it causes the childs content to refresh.
So if you want to use the same logic as SwipeRefreshLayout, just extend the class, and create a method to allow you to pass in the handle to your scrollable view. Note the stock implementation uses canScrollVertically() which does exactly what we want, but only appears in SDK >= 14.
Also don't forget to include the constructor that contains the param "AttributeSet", when you extend the class, otherwise you will have problems using the class in your layout files.
So, in the onCreate method of your Activity (in my case it was an ActionBarActivity) that includes the list view, just call setMyScrollableView passing in your ListView or whatever view you use that scrolls.
/*Constructor*/
public MySwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
}
View mMyScrollableView = null; //The content that get's pulled down.
/*Method used to pass in my scrollable view*/
public void setMyScrollableView(View view){
mMyScrollableView = view;
}
/**
* #return Whether it is possible for the child view of this layout to
* scroll up. This was taken for the most part directly from SwipeRefreshLayout
*/
public boolean canChildScrollUp() {
if(mMyScrollableView == null)
return false;
if (android.os.Build.VERSION.SDK_INT < 14) {
if (mMyScrollableView instanceof AbsListView) {
final AbsListView absListView = (AbsListView) mMyScrollableView;
return absListView.getChildCount() > 0
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
.getTop() < absListView.getPaddingTop());
} else {
return mMyScrollableView.getScrollY() > 0;
}
} else {
return ViewCompat.canScrollVertically(mMyScrollableView, -1);
}
}
The solution from #User22791 works perfectly and, based on that, I created a library available on github that you can use (and modify) for make the usage of swipeRefreshLayout easier for developers. It's here: https://github.com/dagova/referencedSwipeRefreshLayout
Basically you just have to reference in your layout the view to be checked in the method canChildScrollUp. I hope it will be useful.
I also found the other answers didn't quite work.
Took me a while of head scratching to figure out that they are using the method getScrollY() which as this answer explains, is a View method describing how far it's been scroll within a container, not a method to describe how much your Scroll container has been scrolled.
If you use the same technique as in the other answers (overriding the canChildScrollUp() method) you can easily check if the Scrollable is at it's highest point:
#Override
public boolean canChildScrollUp() {
return !isListAtTop();
}
private boolean isListAtTop() {
if(mGridView.getChildCount() == 0) return true;
return mGridView.getChildAt(0).getTop() == 0;
}
(As you can see, I'm using a GridView, but you can use a ListView too)
Easier solution is to use onScrollListener and check if user can see firstElement.
someView.setOnScrollListener(new AbsListView.OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView absListView, int scrollState) {
if (scrollState == SCROLL_STATE_IDLE) {
if (isViewAtTop()) {
swipeLayout.setEnabled(true);
} else {
swipeLayout.setEnabled(false);
}
}
}
#Override
public void onScroll(AbsListView absListView, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (firstVisibleItem == 0) {
swipeLayout.setEnabled(true);
} else {
swipeLayout.setEnabled(false);
}
}
});
Where method isViewAtTop() is some other method, that checks this View is scrolled to the top
Ok I have got it working. If the SwipeRefreshLayout is the root of the layout and the ScrollView resides deep into the hierarchy (I had put the ScrollView inside a RelativeLayout) and not the direct child of the SwipeRefreshLayout, it won’t detect a swipe up on the ScrollView 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 ScrollView scrollview;
public CustomSwipeRefreshLayout(Context context) {
super(context);
}
public CustomSwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setView(ScrollView view) {
this.scrollview = view;
}
#Override
public boolean canChildScrollUp() {
return scrollview.getScrollY() != 0;
}
}