How to add scrolling for a custom view in Android - android

I've just started writing a custom view in Android (for the first time) and I've realised that I need to implement a scrolling feature.
The custom view also uses a header containing some text (which should stay fixed and not scroll).
I've read through the documentation on GestureDetector.SimpleOnGestureListener and Scroller. I also read the documentation on Animating a Scroll Gesture but I found the examples difficult to understand. I've also looked at other questions on Stack Overflow which helped a little.
Using what I understood from the documentation with the Stack Overflow answer as a reference to guide me, I have added the following to my custom view:
Variables and fields:
private OverScroller mScroller;
private final GestureDetector mGestureDetector =
new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
// Note 0 as the x-distance to prevent horizontal scrolling
scrollBy(0, (int) distanceY);
return true;
}
#Override
public boolean onFling(MotionEvent e1, MotionEvent e2,
float velocityX, float velocityY) {
final int maxScrollX = 0;
// wholeViewHeight is height of everything that is drawn
int wholeViewHeight = calculateWholeHeight();
int visibleHeight = getHeight();
final int maxScrollY = wholeViewHeight - visibleHeight;
mScroller.forceFinished(true);
mScroller.fling(0, // No startX as there is no horizontal scrolling
getScrollY(),
0, // No velocityX as there is no horizontal scrolling
- (int) velocityY,
0,
maxScrollX,
0,
maxScrollY);
invalidate();
return true;
}
#Override
public boolean onDown(MotionEvent e) {
if (!mScroller.isFinished()) {
mScroller.forceFinished(true);
}
return true;
}
});
Initialization of mScroller:
// Called from the constructor
private void init() {
mScroller = new OverScroller(getContext(), new FastOutLinearInInterpolator());
...
}
Stuff in onDraw():
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
...
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
}
}
Stuff in onTouchEvent():
#Override
public boolean onTouchEvent(MotionEvent event) {
return mGestureDetector.onTouchEvent(event);
}
The result of these additions is a custom view which can scroll vertically (and not horizontally), however there are a few issues:
I can scroll further past what is drawn
There is no edge glow effect as I reach the end of the custom view (I mean like a RecyclerView or ScrollView)
All of the custom view scrolls as opposed to just a certain part of it
I don't fully understand what is going on
Could someone explain how scrolling works in a custom view and how to implement it properly with these features?

May I offer a simple layout that has a fixed header and vertically scrolling content that sounds like it will do what you want and does not require complex programming? It may save you from having to spend hours of research and programming in the long run:
<?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">
<FrameLayout
android:id="#+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fillViewport="true">
<FrameLayout
android:id="#+id/scrolling_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.v4.widget.NestedScrollView>
</LinearLayout>
Using this layout, you would replace or insert inside the FrameLayout with ID "header" with your fixed header view. You would then put your scolling content inside of the FrameLayout with ID "scrolling_content". The NestedScrollingView would be located directly below your fixed header and automatically give you the scrolling behavior you want without any further code.

Related

Android - Send touch event to other views

I am trying to achieve a scroll effect, I think it can be done because I see some apps implemented this.
I have a FrameLayout, in this layout I have:
- A recycler view
- A float view
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<LinearLayout // float layout here
android:layout_width="match_parent"
android:layout_height="100dp">
</LinearLayout>
</FrameLayout>
When I scroll the recycler view, I can see the float view scroll also, but when it reaches the top of the screen, I want it to stop there. I have successfully implemented this but after that I face to a new issue. Because the float view is above the recycler view, I can not scroll when touch and scroll the float view. In this case the float view seems consumes the touch event so that the recycler does nothing.
What I want to achieve is when user want to scroll the recycler view should consume it.
Im thinking of sending the float view's touch event to recycler view.
Thanks.
I have found same problem some time ago. Here is my solution (it is a little bit hacky, but didn't find better solution). Put in in your custom FrameLayout class:
public class CustomFrameLayout extends FrameLayout {
...
#InjectView(R.id.rv_details)
RecyclerView recyclerView;
#InjectView(R.id.ll_details_action_bar_wrapper)
ViewGroup actionBarWrapperViewGroup;
private List<MotionEvent> cachedEventList = new ArrayList<>();
private boolean touchIsFromActionBar;
private boolean yTranslationThresholdPassed;
// Pawel Janeczek
// Those two overrides is for forwarding touch events, that started on action bar, to recyclerview.
// But you may ask, why there are so many lines? it should by only recyclerView.dispatchTouchEvent(ev) and it should be fine
// It is because RecyclerView when it is starting scrolling it sends parent.requestDisallowInterceptTouchEvent which disables sending onInterceptTouchEvent to parent
// In such case we must set a flag touchIsFromActionBar when motion event starts and is in action bar, and then when this flag is set we remove calling super on requestDisallowInterceptTouchEvent
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if (action == MotionEvent.ACTION_DOWN && viewUtils.isWithinViewBounds(actionBarWrapperViewGroup, ev.getRawX(), ev.getRawY())) {
touchIsFromActionBar = true;
}
if (touchIsFromActionBar && shouldDispatchEventToRecyclerView(ev)) {
if (!listUtils.isEmpty(cachedEventList)) {
for (MotionEvent motionEvent : cachedEventList) {
recyclerView.dispatchTouchEvent(motionEvent);
}
cachedEventList.clear();
}
recyclerView.dispatchTouchEvent(ev);
}
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
cachedEventList.clear();
yTranslationThresholdPassed = false;
touchIsFromActionBar = false;
}
return false;
}
private boolean shouldDispatchEventToRecyclerView(MotionEvent event) {
if (yTranslationThresholdPassed) {
return true;
} else if (listUtils.isEmpty(cachedEventList)) {
cachedEventList.add(MotionEvent.obtain(event));
return false;
}
int yTranslationThreshold = 2;
MotionEvent lastEvent = listUtils.getLast(cachedEventList);
if (Math.abs(lastEvent.getY() - event.getY()) > yTranslationThreshold) {
yTranslationThresholdPassed = true;
return true;
} else {
cachedEventList.add(MotionEvent.obtain(event));
return false;
}
}
#Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (!touchIsFromActionBar) {
super.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
...
}
ViewGroup named actionBarWrapperViewGroup is a flow layout in your sample.
And xml for CustomFrameLayout:
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<RecyclerView
android:id="#+id/rv_details"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
<LinearLayout
android:id="#+id/ll_details_action_bar_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
...
<FrameLayout
android:id="#+id/ll_details_action_bar_container"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="#dimen/default_bar_height"
android:background="?colorPrimary"/>
...
</LinearLayout>
</merge>
It is live copied from my project, so names can be misleading but I think it is understandable. If you have any questions go on.

how to intercept horizontal flings on children of an android ListView

I'd like to respond to horizontal "fling" gestures on individual cells in a vertically scrolling ListView. Currently I've accomplished this by using a GestureDetector for each cell view in the list.
I'm noticing, though, that it's much harder to actually get a horizontal "fling" to register with one of the cell views than if they were just stuck in a non-scrolling linear layout.
For instance, if I tap down inside a cell then drag my finger "up and to the right" fairly quickly this is recognized as a fling in the non-scrolling case, but isn't recognized in the scrolling case.
I've experimented with sub-classing ListView and overriding onInterceptTouchEvent, then I can't seem to get it right. What I would like to have happen is for gestures that will eventually be recognized as "flings" on a child view to be ignored by the scroll view. I would want to limit these based on the "angle" of the gesture, i.e. the ratio of the Y distance to the X distance. If that ratio is sufficiently high then its a "vertical" fling and the ListView should handle it. If that ratio is sufficiently low then it's a "horizontal" fling and the ListView should ignore it and allow a child view to handle it.
Can anyone provide some perspective on how this might be accomplished? I'm assuming I'll going to have to do something clever in the onInterceptTouchEvent method of the ListView sub-class.
try this:
final ListView v = new ListView(this);
ArrayAdapter<String> a = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
a.add("one");
a.add("two");
a.add("three");
a.add("four");
v.setAdapter(a);
OnGestureListener ogl = new SimpleOnGestureListener() {
#Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (Math.abs(velocityX) > Math.abs(velocityY)) {
Log.d(TAG, "onFling " + v.pointToPosition((int) e1.getX(), (int) e1.getY()));
return true;
}
return false;
}
};
final GestureDetector detector = new GestureDetector(ogl);
OnTouchListener otl = new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
return detector.onTouchEvent(event);
}
};
v.setOnTouchListener(otl);
OnItemClickListener oicl = new OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Log.d(TAG, "onItemClick " + position);
}
};
v.setOnItemClickListener(oicl);
setContentView(v);

Two dimensional scrolling... a suggestion that needs feedback

While spending a copious amount of time googling for a relatively simple solution to my problem I found this as a solution for two-dimensional scrolling. I have a a horizontalscrollview nested in a scrollview. I fiddled with this in a few ways and was unsuccessful in making anything functional. Does anyone have any ideas on how to make a concept like this work?
Scrollview scrollY = (ScrollView)findViewById(R.id.scrollY);
LinearLayout scrollYChild = (LinearLayout)findViewById(R.id.scrollYChild);
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
scrollYChild.dispatchTouchEvent(event);
scrollY.onTouchEvent(event);
return true;
}
I have also found this: http://blog.gorges.us/2010/06/android-two-dimensional-scrollview/ but I'm don't understand at all how to implement such a long piece of code properly.
It doesn't make much sense to me that two-dimensional scrolling is inherent in a webview but nonexistent elsewhere... Any and all help is appreciated.
Edit: How exactly does this work when zoomed in on an image in the gallery. Surely there has to be a way to implement that same functionality here.
Im not sure about the blog you have posted, this was my solution:
/**
* This class disables Y-motion on touch event.
* It should only be used as parent class of HorizontalScrollView
*/
public class ParentScrollView extends ScrollView {
private GestureDetector mGestureDetector;
View.OnTouchListener mGestureListener;
#SuppressWarnings("deprecation")
public ParentScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
mGestureDetector = new GestureDetector(new YScrollDetector());
setFadingEdgeLength(0);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if( mGestureDetector.onTouchEvent(ev)&super.onInterceptTouchEvent(ev)){
return true;
}else{
return false;
}
}
// Return false if we're scrolling in the x direction
class YScrollDetector extends SimpleOnGestureListener {
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if(Math.abs(distanceY) > Math.abs(distanceX)) {
return true;
}
return false;
}
}
}
XML:
<com.example.Views.ParentScrollView
android:layout_width="match_parent"
android:layout_height="match_parent" >
<HorizontalScrollView
android:id="#+id/tlDBtable"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
</HorizontalScrollView>
</com.example.Views.ParentScrollView>
Basically the parent scrollview which only scrolls veritcal will be disabled because you will use a new custom class. Then you put a HScrollview within the scroll view. The Parentscroll view will pass the touch even if its not vertical to the horiszontalscroll view which makes it 2D scrolling effect.

How to transfer touch events to the view beneath in a RelativeLayout?

I have a ScrollView on top of another view(with Buttons). The ScrollView is taking the whole screen and is obscuring the view that is beneath it.
At some point in my app I need the ScrollView to be disabled (but still visible) and transfer all the touch events to the Buttons that are beneath the ScrollView. How can I do that? Some views like Buttons are automatically doing that when disabled but a ScrollView is not doing that.
Try to implement your own ScrollView which has a flag to indicate the status(disabled/enabled) and also overrides the onTouchEvent and dispatchTouchEvent to let the touch events get pass the ScrollView. Here is an example:
public class DisabledScrollView extends ScrollView {
private boolean mIsDisable = false;
// if status is true, disable the ScrollView
public void setDisableStatus(boolean status) {
mIsDisable = status;
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
// no more tocuh events for this ScrollView
if (mIsDisable) {
return false;
}
return super.onTouchEvent(ev);
}
public boolean dispatchTouchEvent(MotionEvent ev) {
// although the ScrollView doesn't get touch events , its children will get them so intercept them.
if (mIsDisable) {
return false;
}
return super.dispatchTouchEvent(ev);
}
}
Then all you have to do is change the value of that flag. See if it works.
In my case, I just needed to handle the touch event in View A, which was overlaping View B and then send the event to View B. Both views were child of the same RelativeLayout, but there was no parent-child relation between views A and B. This worked for me:
viewA.setOnTouchListener( new OnTouchListener(){
#Override
public boolean onTouch(View v, MotionEvent event) {
// do my stuff here
viewB.dispatchTouchEvent( event );
}
}
In this case I have a recyclerview under a scrollview. The top of scrollview is in vertical scroll, and the recyclerview is in horizontal scroll. The scrollview have top padding, making the recyclerview is visible through the transparency in the scrollview padding. I have to make it this way because when the scrollview is scrolled the recyclerview will scroll vertically to like parallax effect (this effect is in another code). This code below is working for my case, might help
scrollView.setOnTouchListener(new View.OnTouchListener() {
float mDownX,mDownY;
boolean mIsSwiping,isDown;
#Override
public boolean onTouch(View v, MotionEvent event) {
if(mIsSwiping){
recyclerView.dispatchTouchEvent(event);
}
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mIsSwiping = false;
isDown = true;
mDownX = event.getX();
mDownY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
if(isDown){
float deltaX = Math.abs(event.getX() - mDownX);
float deltaY = Math.abs(event.getY() - mDownY);
mDownX = event.getX();
mDownY = event.getY();
if(deltaX!=deltaY){
isDown = false;
if(deltaX>deltaY){
mIsSwiping = true;
}
}
}
}
return mIsSwiping;
}
});
This is the layout
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/gray0"
android:clipToPadding="false"
android:clipChildren="false"
android:paddingBottom="70dp">
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="320dp"
android:orientation="horizontal"
app:layoutManager="android.support.v7.widget.LinearLayoutManager"/>
<ScrollView
android:id="#+id/scrollview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="301.75dp"
android:paddingBottom="23.5dp"
android:clipChildren="false"
android:clipToPadding="false">
.
.
.

Detect which view your finger is sliding over in Android

While similar questions have been asked in the past they don't seem to really have been answered which might be due to confusion as to what's being asked.
Put simply, I'd like to detect which view is being entered as your finger slides over the screen. The best example of this in action is the soft keyboard on any android phone. When you press any key it shows up as a popup to tell you what letter is under your finger. If you now move your finger over the keyboard in a single gesture the various letters pop up as you move over the various letters of the alphabet.
What listeners are used for this type of behaviour. I've tried OnTouchListeners but they seem to be only when you 'touch' the button as opposed to 'finger past' them
Button button = (Button)findViewById(R.id.button);
button.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {doStuff();}
});
button.setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
doStuff();
return false;
}
});
OnFocusChangeListener don't help either.
create a Layout
add Views to your Layout
set the setOnTouchListener to your Layout
override the onTouch method with the following:
public boolean onTouch(View v, MotionEvent event)
{
LinearLayout layout = (LinearLayout)v;
for(int i =0; i< layout.getChildCount(); i++)
{
View view = layout.getChildAt(i);
Rect outRect = new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
if(outRect.contains((int)event.getX(), (int)event.getY()))
{
// over a View
}
}
}
EDIT:
I saw keyboard. I guess, it just one view and coordinates of every letter is known. So you can easily compute which letter the user slides through
AND NOW THE ANSWER:
I'm not sure, but probably this code helps your.
It's so far away, I wrote it for me. But the idea is following.
If I remember right, there is no gesturedetector for views, but you can combine touchlistener of the view with geturelistener of your activity.
Once you've touched your view, you have
private GestureDetector mGestureDetector;
// x and y coordinates within our view
private static float sideIndexX;
private static float sideIndexY;
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
...
mGestureDetector = new GestureDetector(this, new SideIndexGestureListener());
}
class MyGestureListener extends
GestureDetector.SimpleOnGestureListener
{
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY)
{
// we know already coordinates of first touch
// we know as well a scroll distance
sideIndexX = sideIndexX - distanceX;
sideIndexY = sideIndexY - distanceY;
// when the user scrolls within our side index
// we can show for every position in it a proper
// item in the country list
if (sideIndexX >= 0 && sideIndexY >= 0)
{
doStuff();
}
return super.onScroll(e1, e2, distanceX, distanceY);
}
}
button.setOnTouchListener(new OnTouchListener()
{
#Override
public boolean onTouch(View v, MotionEvent event)
{
// now you know coordinates of touch
// store them
sideIndexX = event.getX();
sideIndexY = event.getY();
doStuff();
return false;
}
});
You may want to try GestureDetector.
http://android-developers.blogspot.com/2010/06/making-sense-of-multitouch.html
it's geared to multitouch, but this is a good start toward understanding android touch/gestures, next stop, api docs/samples
The simple answer is you can't - not like the iPhone when in accessibility mode.
Until Ice Cream Sandwich that is. It now has the iPhone-like capability of being able to identify elements under your finger without having to lift it.
It's fairly straight forward to handle this manually.
Using your parent layout as the onTouchListener (in the following example, I extend a RelativeLayout), you can check for collisions between a MotionEvent and the child Views using simple co-ordinate comparison logic:
/** Returns the View colliding with the TouchEvent. */
private final View getCollisionWith(final MotionEvent pMotionEvent) {
// Declare the LocationBuffer.
final int[] lLocationBuffer = new int[2];
// Iterate the children.
for(int i = 0; i < this.getChildCount(); i++) { /** TODO: Order. */
// Fetch the child View.
final View lView = this.getChildAt(i);
// Fetch the View's location.
lView.getLocationOnScreen(lLocationBuffer);
// Is the View colliding?
if(pMotionEvent.getRawX() > lLocationBuffer[0] && pMotionEvent.getRawX() < lLocationBuffer[0] + lView.getWidth() && pMotionEvent.getRawY() > lLocationBuffer[1] && pMotionEvent.getRawY() < lLocationBuffer[1] + lView.getHeight()) {
// Return the colliding View.
return lView;
}
}
// We couldn't find a colliding View.
return null;
}
Calls to getCollisionWith will return View references that may be manipulated arbitrarily.

Categories

Resources