Manage touch events in a ViewGroup

Try the Compose way
Jetpack Compose is the recommended UI toolkit for Android. Learn about gestures in Compose.

Handling touch events in a ViewGroup takes special care because it's common for a ViewGroup to have children that are targets for different touch events than the ViewGroup itself. To make sure each view correctly receives the touch events intended for it, override the onInterceptTouchEvent() method.

Refer to the following related resources:

Intercept touch events in a ViewGroup

The onInterceptTouchEvent() method is called whenever a touch event is detected on the surface of a ViewGroup, including on the surface of its children. If onInterceptTouchEvent() returns true, the MotionEvent is intercepted, meaning it isn't passed onto the child but rather to the onTouchEvent() method of the parent.

The onInterceptTouchEvent() method gives a parent the chance to see touch events before its children do. If you return true from onInterceptTouchEvent(), the child view that was previously handling touch events receives an ACTION_CANCEL , and the events from that point forward are sent to the parent's onTouchEvent() method for the usual handling. onInterceptTouchEvent() can also return false and spy on events as they travel down the view hierarchy to their usual targets, which handle the events with their own onTouchEvent().

In the following snippet, the MyViewGroup class extends ViewGroup. MyViewGroup contains multiple child views. If you drag your finger across a child view horizontally, the child view no longer gets touch events, and MyViewGroup handles touch events by scrolling its contents. However, if you tap buttons in the child view, or scroll the child view vertically, the parent doesn't intercept those touch events because the child is the intended target. In those cases, onInterceptTouchEvent() returns false, and the MyViewGroup class's onTouchEvent() isn't called.

Kotlin

classMyViewGroup@JvmOverloadsconstructor(
context:Context,
privatevalmTouchSlop:Int=ViewConfiguration.get(context).scaledTouchSlop
):ViewGroup(context){
...
overridefunonInterceptTouchEvent(ev:MotionEvent):Boolean{
// This method only determines whether you want to intercept the motion.
// If this method returns true, onTouchEvent is called and you can do
// the actual scrolling there.
returnwhen(ev.actionMasked){
// Always handle the case of the touch gesture being complete.
MotionEvent.ACTION_CANCEL,MotionEvent.ACTION_UP->{
// Release the scroll.
mIsScrolling=false
false// Don't intercept the touch event. Let the child handle it.
}
MotionEvent.ACTION_MOVE->{
if(mIsScrolling){
// You're currently scrolling, so intercept the touch event.
true
}else{
// If the user drags their finger horizontally more than the
// touch slop, start the scroll.
// Left as an exercise for the reader.
valxDiff:Int=calculateDistanceX(ev)
// Touch slop is calculated using ViewConfiguration constants.
if(xDiff > mTouchSlop){
// Start scrolling!
mIsScrolling=true
true
}else{
false
}
}
}
...
else->{
// In general, don't intercept touch events. The child view
// handles them.
false
}
}
}
overridefunonTouchEvent(event:MotionEvent):Boolean{
// Here, you actually handle the touch event. For example, if the action
// is ACTION_MOVE, scroll this container. This method is only called if
// the touch event is intercepted in onInterceptTouchEvent.
...
}
}

Java

publicclass MyViewGroupextendsViewGroup{
privateintmTouchSlop;
...
ViewConfigurationvc=ViewConfiguration.get(view.getContext());
mTouchSlop=vc.getScaledTouchSlop();
...
@Override
publicbooleanonInterceptTouchEvent(MotionEventev){
// This method only determines whether you want to intercept the motion.
// If this method returns true, onTouchEvent is called and you can do
// the actual scrolling there.
finalintaction=MotionEventCompat.getActionMasked(ev);
// Always handle the case of the touch gesture being complete.
if(action==MotionEvent.ACTION_CANCEL||action==MotionEvent.ACTION_UP){
// Release the scroll.
mIsScrolling=false;
returnfalse;// Don't intercept touch event. Let the child handle it.
}
switch(action){
caseMotionEvent.ACTION_MOVE:{
if(mIsScrolling){
// You're currently scrolling, so intercept the touch event.
returntrue;
}
// If the user drags their finger horizontally more than the
// touch slop, start the scroll.
// Left as an exercise for the reader.
finalintxDiff=calculateDistanceX(ev);
// Touch slop is calculated using ViewConfiguration constants.
if(xDiff > mTouchSlop){
// Start scrolling.
mIsScrolling=true;
returntrue;
}
break;
}
...
}
// In general, don't intercept touch events. The child view handles them.
returnfalse;
}
@Override
publicbooleanonTouchEvent(MotionEventev){
// Here, you actually handle the touch event. For example, if the
// action is ACTION_MOVE, scroll this container. This method is only
// called if the touch event is intercepted in onInterceptTouchEvent.
...
}
}

Note that ViewGroup also provides a requestDisallowInterceptTouchEvent() method. The ViewGroup calls this method when a child doesn't want the parent and its ancestors to intercept touch events with onInterceptTouchEvent().

Process ACTION_OUTSIDE events

If a ViewGroup receives a MotionEvent with an ACTION_OUTSIDE , the event isn't dispatched to its children by default. To process a MotionEvent with ACTION_OUTSIDE, either override dispatchTouchEvent(MotionEvent event) to dispatch to the appropriate View or handle it in the relevant Window.Callback —for example, Activity .

Use ViewConfiguration constants

The preceding snippet uses the current ViewConfiguration to initialize a variable called mTouchSlop. You can use the ViewConfiguration class to access common distances, speeds, and times used by the Android system.

"Touch slop" refers to the distance in pixels a user's touch can wander before the gesture is interpreted as scrolling. Touch slop is typically used to prevent accidental scrolling when the user is performing another touch operation, such as touching on-screen elements.

Two other commonly used ViewConfiguration methods are getScaledMinimumFlingVelocity() and getScaledMaximumFlingVelocity() . These methods return the minimum and maximum velocity, respectively, to initiate a fling measured in pixels per second. For example:

Kotlin

privatevalvc:ViewConfiguration=ViewConfiguration.get(context)
privatevalmSlop:Int=vc.scaledTouchSlop
privatevalmMinFlingVelocity:Int=vc.scaledMinimumFlingVelocity
privatevalmMaxFlingVelocity:Int=vc.scaledMaximumFlingVelocity
...
MotionEvent.ACTION_MOVE->{
...
valdeltaX:Float=motionEvent.rawX-mDownX
if(Math.abs(deltaX) > mSlop){
// A swipe occurs, do something.
}
returnfalse
}
...
MotionEvent.ACTION_UP->{
...
if(velocityXinmMinFlingVelocity..mMaxFlingVelocity && velocityY < velocityX){
// The criteria are satisfied, do something.
}
}

Java

ViewConfigurationvc=ViewConfiguration.get(view.getContext());
privateintmSlop=vc.getScaledTouchSlop();
privateintmMinFlingVelocity=vc.getScaledMinimumFlingVelocity();
privateintmMaxFlingVelocity=vc.getScaledMaximumFlingVelocity();
...
caseMotionEvent.ACTION_MOVE:{
...
floatdeltaX=motionEvent.getRawX()-mDownX;
if(Math.abs(deltaX) > mSlop){
// A swipe occurs, do something.
}
...
caseMotionEvent.ACTION_UP:{
...
}if(mMinFlingVelocity<=velocityX && velocityX<=mMaxFlingVelocity
 && velocityY < velocityX){
// The criteria are satisfied, do something.
}
}

Extend a child view's touchable area

Android provides the TouchDelegate class to make it possible for a parent to extend the touchable area of a child view beyond the child's bounds. This is useful when the child has to be small but needs a larger touch region. You can also use this approach to shrink the child's touch region.

In the following example, an ImageButton is the _delegate view_—that is, the child whose touch area the parent extends. Here is the layout file:

<RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/parent_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ImageButtonandroid:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@null"
android:src="@drawable/icon"/>
</RelativeLayout>

The following snippet completes these tasks:

  • Gets the parent view and posts a Runnable on the UI thread. This makes sure that the parent lays out its children before calling the getHitRect() method. The getHitRect() method gets the child's hit rectangle (or touchable area) in the parent's coordinates.
  • Finds the ImageButton child view and calls getHitRect() to get the bounds of the child's touchable area.
  • Extends the bounds of the ImageButton child view's hit rectangle.
  • Instantiates a TouchDelegate, passing in the expanded hit rectangle and the ImageButton child view as parameters.
  • Sets the TouchDelegate on the parent view so that touches within the touch delegate bounds are routed to the child.

In its capacity as touch delegate for the ImageButton child view, the parent view receives all touch events. If the touch event occurs within the child's hit rectangle, the parent passes the touch event to the child for handling.

Kotlin

publicclassMainActivity:Activity(){
overridefunonCreate(savedInstanceState:Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Post in the parent's message queue to make sure the parent lays out
// its children before you call getHitRect().
findViewById<View>(R.id.parent_layout).post{
// The bounds for the delegate view, which is an ImageButton in this
// example.
valdelegateArea=Rect()
valmyButton=findViewById<ImageButton>(R.id.button).apply{
isEnabled=true
setOnClickListener{
Toast.makeText(
this@MainActivity,
"Touch occurred within ImageButton touch region.",
Toast.LENGTH_SHORT
).show()
}
// The hit rectangle for the ImageButton.
getHitRect(delegateArea)
}
// Extend the touch area of the ImageButton beyond its bounds on the
// right and bottom.
delegateArea.right+=100
delegateArea.bottom+=100
// Set the TouchDelegate on the parent view so that touches within
// the touch delegate bounds are routed to the child.
(myButton.parentas?View)?.apply{
// Instantiate a TouchDelegate. "delegateArea" is the bounds in
// local coordinates of the containing view to be mapped to the
// delegate view. "myButton" is the child view that receives
// motion events.
touchDelegate=TouchDelegate(delegateArea,myButton)
}
}
}
}

Java

publicclass MainActivityextendsActivity{
@Override
protectedvoidonCreate(BundlesavedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Get the parent view.
ViewparentView=findViewById(R.id.parent_layout);
parentView.post(newRunnable(){
// Post in the parent's message queue to make sure the parent lays
// out its children before you call getHitRect().
@Override
publicvoidrun(){
// The bounds for the delegate view, which is an ImageButton in
// this example.
RectdelegateArea=newRect();
ImageButtonmyButton=(ImageButton)findViewById(R.id.button);
myButton.setEnabled(true);
myButton.setOnClickListener(newView.OnClickListener(){
@Override
publicvoidonClick(Viewview){
Toast.makeText(MainActivity.this,
"Touch occurred within ImageButton touch region.",
Toast.LENGTH_SHORT).show();
}
});
// The hit rectangle for the ImageButton.
myButton.getHitRect(delegateArea);
// Extend the touch area of the ImageButton beyond its bounds on
// the right and bottom.
delegateArea.right+=100;
delegateArea.bottom+=100;
// Instantiate a TouchDelegate. "delegateArea" is the bounds in
// local coordinates of the containing view to be mapped to the
// delegate view. "myButton" is the child view that receives
// motion events.
TouchDelegatetouchDelegate=newTouchDelegate(delegateArea,
myButton);
// Set the TouchDelegate on the parent view so that touches
// within the touch delegate bounds are routed to the child.
if(View.class.isInstance(myButton.getParent())){
((View)myButton.getParent()).setTouchDelegate(touchDelegate);
}
}
});
}
}

Content and code samples on this page are subject to the licenses described in the Content License. Java and OpenJDK are trademarks or registered trademarks of Oracle and/or its affiliates.

Last updated 2025年12月29日 UTC.