On Click event for Parent and Child view - android

I have a RelativeLayout and an ImageView.
The layout is given below -
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/rlMain"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/black" >
<ImageView
android:id="#+id/ivIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#drawable/ic_launcher" />
</RelativeLayout>
The Activity code is -
public class MainActivity extends Activity implements OnClickListener{
private RelativeLayout rlMain;
private ImageView ivIcon;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
rlMain = (RelativeLayout) findViewById(R.id.rlMain);
ivIcon = (ImageView) findViewById(R.id.ivIcon);
rlMain.setOnClickListener(this);
ivIcon.setOnClickListener(this);
}
#Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.rlMain:
Toast.makeText(this, "Relative Layout clicked", Toast.LENGTH_SHORT).show();
break;
case R.id.ivIcon:
Toast.makeText(this, "Image View clicked", Toast.LENGTH_SHORT).show();
break;
}
}
}
I have applied the on click listener for both the RelativeLayout (parent) and the ImageView (child).
While clicking on the relative layout - it is handled by the relative layout click handler.(This seems to be correct).
While clicking on the Imagview - It is handled by the imageview. (Here is the confusion).
Should the click on the imageview get handled by both the parent(relative layout) and child (imageview)?
What is the logic if only the child view is handling the click?

A clickEvent will be delivered to the lowest child element in the layout hierarchy. If this element does not have an onClick behaviour it will pass the event up to its parent until the event gets handled.
Therefore you can treat the LinearLayout as one single block for your onClick behaviour. If you create another clickable element inside the layout be sure to make it big enough to reduce the chance of the user missing the correct item.
Source: Does making parent clickable make all child element clickable as well?

Normally in cases where the image to be clicked is very small in dimension the imageView is kept inside a layout which is given some padding, then the click of the layout is handled. This gives enough area on screen for the user to click on

Override dispatchTouchEvent function
Get the touching point
Get the view's position. You can use getGlobalVisibleRect() or getLocalVisibleRect() to detect view's position on your needs.
Set simple if statement and find the location of point is in the ImageView or not.
Action should be MotionEvent.ACTION_DOWN to get only first touch in the case of user make slide action on the screen.
The Kotlin Code:
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
if(ev?.action == MotionEvent.ACTION_DOWN) {
val pointX = ev.x
val pointY = ev.y
val locationIvIcon = Rect()
if(iv_icon.getGlobalVisibleRect(locationIvIcon)) {
if(isInChildView(pointX, pointY, locationSendGiftPopup)) {
// Do action for ImageView
} else {
// Do action for RelativeLayout
}
}
}
return super.dispatchTouchEvent(ev)
}
private fun isInChildView(x: Float?, y: Float?, location: Rect): Boolean {
x?.let {
y?.let {
return x >= location.left && x <= location.right
&& y >= location.top && y <= location.bottom
}
}
return false
}

Related

Allow BottomSheet to slide up after threshold is reached on an area outside

I am trying to replicate a behavior that the current Google Maps has which allows the bottom sheet to be revealed when sliding up from the bottom bar.
Notice in the recording below that I first tap on one of the buttons at the bottom bar and then slide up, which in turn reveals the sheet behind it.
I cannot find anywhere explained how something like this can be achieved. I tried exploring the BottomSheetBehavior and customizing it, but nowhere I can find a way to track the initial tap and then let the sheet take over the movement once the touch slop threshold is reached.
How can I achieve this behavior without resorting to libraries? Or are there any official Google/Android views that allow this behavior between two sections (the navigation bar and bottom sheet)?
Took some time but I found a solution based on examples and discussion provided by two authors, their contributions can be found here:
https://gist.github.com/davidliu/c246a717f00494a6ad237a592a3cea4f
https://github.com/gavingt/BottomSheetTest
The basic logic is to handle touch events in onInterceptTouchEvent in a custom BottomSheetBehavior and check in a CoordinatorLayout if the given view (from now on named proxy view) is of interest for the rest of the touch delegation in isPointInChildBounds.
This can be adapted to use more than one proxy view if needed, the only change necessary for this is to make a proxy view list and iterate the list instead of using a single proxy view reference.
Below follows the code example of this implementation. Do note that this is only configured to handle vertical movements, if horizontal movements are necessary then adapt the code to your need.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<com.example.tabsheet.CustomCoordinatorLayout
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/customCoordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.google.android.material.tabs.TabLayout
android:id="#+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="#android:color/darker_gray">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="#drawable/ic_launcher_background"
android:text="Tab 1" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="#drawable/ic_launcher_background"
android:text="Tab 2" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="#drawable/ic_launcher_background"
android:text="Tab 3" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="#drawable/ic_launcher_background"
android:text="Tab 4" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="#drawable/ic_launcher_background"
android:text="Tab 5" />
</com.google.android.material.tabs.TabLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="#+id/bottomSheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#3F51B5"
android:clipToPadding="false"
app:behavior_peekHeight="0dp"
app:layout_behavior=".CustomBottomSheetBehavior" />
</com.example.tabsheet.CustomCoordinatorLayout>
MainActivity.java
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
final CustomCoordinatorLayout customCoordinatorLayout;
final CoordinatorLayout bottomSheet;
final TabLayout tabLayout;
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
customCoordinatorLayout = findViewById(R.id.customCoordinatorLayout);
bottomSheet = findViewById(R.id.bottomSheet);
tabLayout = findViewById(R.id.tabLayout);
iniList(bottomSheet);
customCoordinatorLayout.setProxyView(tabLayout);
}
private void iniList(final ViewGroup parent) {
#ColorInt int backgroundColor;
final int padding;
final int maxItems;
final float density;
final NestedScrollView nestedScrollView;
final LinearLayout linearLayout;
final ColorDrawable dividerDrawable;
int i;
TextView textView;
ViewGroup.LayoutParams layoutParams;
density = Resources.getSystem().getDisplayMetrics().density;
padding = (int) (20 * density);
maxItems = 50;
backgroundColor = ContextCompat.getColor(this, android.R.color.holo_blue_bright);
dividerDrawable = new ColorDrawable(Color.WHITE);
layoutParams = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
);
nestedScrollView = new NestedScrollView(this);
nestedScrollView.setLayoutParams(layoutParams);
nestedScrollView.setClipToPadding(false);
nestedScrollView.setBackgroundColor(backgroundColor);
linearLayout = new LinearLayout(this);
linearLayout.setLayoutParams(layoutParams);
linearLayout.setOrientation(LinearLayout.VERTICAL);
linearLayout.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
linearLayout.setDividerDrawable(dividerDrawable);
for (i = 0; i < maxItems; i++) {
textView = new TextView(this);
textView.setText("Item " + (1 + i));
textView.setPadding(padding, padding, padding, padding);
linearLayout.addView(textView, layoutParams);
}
nestedScrollView.addView(linearLayout);
parent.addView(nestedScrollView);
}
}
CustomCoordinatorLayout.java
public class CustomCoordinatorLayout extends CoordinatorLayout {
private View proxyView;
public CustomCoordinatorLayout(#NonNull Context context) {
super(context);
}
public CustomCoordinatorLayout(
#NonNull Context context,
#Nullable AttributeSet attrs
) {
super(context, attrs);
}
public CustomCoordinatorLayout(
#NonNull Context context,
#Nullable AttributeSet attrs,
int defStyleAttr
) {
super(context, attrs, defStyleAttr);
}
#Override
public boolean isPointInChildBounds(
#NonNull View child,
int x,
int y
) {
if (super.isPointInChildBounds(child, x, y)) {
return true;
}
// we want to intercept touch events if they are
// within the proxy view bounds, for this reason
// we instruct the coordinator layout to check
// if this is true and let the touch delegation
// respond to that result
if (proxyView != null) {
return super.isPointInChildBounds(proxyView, x, y);
}
return false;
}
// for this example we are only interested in intercepting
// touch events for a single view, if more are needed use
// a List<View> viewList instead and iterate in
// isPointInChildBounds
public void setProxyView(View proxyView) {
this.proxyView = proxyView;
}
}
CustomBottomSheetBehavior.java
public class CustomBottomSheetBehavior<V extends View> extends BottomSheetBehavior<V> {
// we'll use the device's touch slop value to find out when a tap
// becomes a scroll by checking how far the finger moved to be
// considered a scroll. if the finger moves more than the touch
// slop then it's a scroll, otherwise it is just a tap and we
// ignore the touch events
private int touchSlop;
private float initialY;
private boolean ignoreUntilClose;
public CustomBottomSheetBehavior(
#NonNull Context context,
#Nullable AttributeSet attrs
) {
super(context, attrs);
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
#Override
public boolean onInterceptTouchEvent(
#NonNull CoordinatorLayout parent,
#NonNull V child,
#NonNull MotionEvent event
) {
// touch events are ignored if the bottom sheet is already
// open and we save that state for further processing
if (getState() == STATE_EXPANDED) {
ignoreUntilClose = true;
return super.onInterceptTouchEvent(parent, child, event);
}
switch (event.getAction()) {
// this is the first event we want to begin observing
// so we set the initial value for further processing
// as a positive value to make things easier
case MotionEvent.ACTION_DOWN:
initialY = Math.abs(event.getRawY());
return super.onInterceptTouchEvent(parent, child, event);
// if the last bottom sheet state was not open then
// we check if the current finger movement has exceed
// the touch slop in which case we return true to tell
// the system we are consuming the touch event
// otherwise we let the default handling behavior
// since we don't care about the direction of the
// movement we ensure its difference is a positive
// integer to simplify the condition check
case MotionEvent.ACTION_MOVE:
return !ignoreUntilClose
&& Math.abs(initialY - Math.abs(event.getRawY())) > touchSlop
|| super.onInterceptTouchEvent(parent, child, event);
// once the tap or movement is completed we reset
// the initial values to restore normal behavior
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
initialY = 0;
ignoreUntilClose = false;
return super.onInterceptTouchEvent(parent, child, event);
}
return super.onInterceptTouchEvent(parent, child, event);
}
}
Result with transparent status bar and navigation bar to help visualize the bottom sheet sliding up, but excluded from the code above since it was not relevant for this question.
Note: It is possible you might not even need a custom bottom sheet behavior if your bottom sheet layout contains a certain scrollable view type (NestedScrollView for example) that can be used as is by the CoordinatorLayout, so try without the custom bottom sheet behavior once your layout is ready since it will make this simpler.
You could try something like this (It's Pseudocode, hopefully you understand what I'm getting at):
<FrameLayout id="+id/bottomSheet">
<View id="exploreNearby bottomMargin="buttonContainerHeight/>
<LinearLayout>
<Button id="explore"/>
<Button id="explore"/>
<Button id="explore"/>
</LinearLayout>
<View width="match" height="match" id="+id/touchCatcher"
</FrameLayout>
Add a gesture detector on the bottomSheet view on override onTouch(). which uses SimpleOnGestureListener to wait for a "scroll" events - everything but a scroll event you can replicate down through to the view as normal.
On a scroll event you can grow your exploreNearby as a delta (make sure it doesn't recurse or go to high or too low).
The Bottom sheet class will already do this for you. Just set it's peek height to 0 and it should already listen for the slide up gesture.
However, I'm not positive it will work with a peek height of 0. So if that doesn't work, simply put a peek height of 20dp and make the top portion of the bottom sheet layout transparent so it is not visible.
That should do the trick for ya, unless I'm misunderstanding your question. If your goal is to simply be able to tap at the bottom and slide upwards bringing up the bottom sheet that should be pretty straight forward.
The one possible issue that you "could" encounter is if the bottom sheet doesn't receive the touch events due to the button already consuming it. If this happens you will need to create a touch handler for the whole screen and return "true" that you are handling it each time, then simply forward the touch events to the underlying view, so when you get above the threshold of your bottom tab bar you start sending the touch events to the bottom sheet layout instead of the tab bar.
It sounds harder than it is. Most classes have an onTouch and you just forward it on. However, only go that route, if it doesn't work for you out of the box the way I described in the first two scenarios.
Lastly, one other option that might work is to create your tab buttons as part of the bottomSheetLayout and make the peek height equivalent of the tab bar. Then make sure the tab bar is constrained to bottomsheet parent bottom, so that when you swipe up it simply stays at the bottom. This would enable you to click the buttons or get the free bottom sheet behavior.
Happy Coding!

Android Floating activity with dismiss on swipe

The latest Facebook's android app has a very nice floating comment window. There the user can dismiss the window swiping up or down making it really ease to use.
I want to implement a similar behaviour in my app but I don't know how to do it. Any idea or clue about how to do it will be really appreciated.
Screenshots of the Facebook app
(sorry, the Facebook app from where I took the screenshots is in Japanese)
I write some code that match this closing/resizing behaviour, I don't know if it's the way to go but my code is based on Activity class. First thing I do is create an activity and give it Transluscenttheme to get an activity with transparent background.
In my manifest.xml :
<activity
android:name=".PopupActivity"
android:label="#string/title_activity_popup"
<!-- Use Translucent theme to get transparent activity background
and NoTitleBar to avoid super old style title bar ;) -->
android:theme="#android:style/Theme.Translucent.NoTitleBar">
</activity>
Then I create a simple layout file containing a textview (corresponding to Facebook tchatting part) and a view (corresponding to Facebook "Write your msg"/"send smiley" tab)
my layout/activity_popup.xml :
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/base_popup_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
android:background="#android:color/darker_gray"
android:layout_marginBottom="124dp">
<TextView
android:text="#string/hello_world"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="#android:color/black"/>
<View
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_alignParentBottom="true"
android:background="#android:color/holo_blue_dark"/>
</RelativeLayout>
Finally I handle touch and move event in my PopupActivity class, I use onTouchListener which provide callback in onTouch method.
PopupActivity
public class PopupActivity extends Activity implements View.OnTouchListener{
private RelativeLayout baseLayout;
private int previousFingerPosition = 0;
private int baseLayoutPosition = 0;
private int defaultViewHeight;
private boolean isClosing = false;
private boolean isScrollingUp = false;
private boolean isScrollingDown = false;
#Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_popup);
baseLayout = (RelativeLayout) findViewById(R.id.base_popup_layout);
baseLayout.setOnTouchListener(this);
}
public boolean onTouch(View view, MotionEvent event) {
// Get finger position on screen
final int Y = (int) event.getRawY();
// Switch on motion event type
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
// save default base layout height
defaultViewHeight = baseLayout.getHeight();
// Init finger and view position
previousFingerPosition = Y;
baseLayoutPosition = (int) baseLayout.getY();
break;
case MotionEvent.ACTION_UP:
// If user was doing a scroll up
if(isScrollingUp){
// Reset baselayout position
baseLayout.setY(0);
// We are not in scrolling up mode anymore
isScrollingUp = false;
}
// If user was doing a scroll down
if(isScrollingDown){
// Reset baselayout position
baseLayout.setY(0);
// Reset base layout size
baseLayout.getLayoutParams().height = defaultViewHeight;
baseLayout.requestLayout();
// We are not in scrolling down mode anymore
isScrollingDown = false;
}
break;
case MotionEvent.ACTION_MOVE:
if(!isClosing){
int currentYPosition = (int) baseLayout.getY();
// If we scroll up
if(previousFingerPosition >Y){
// First time android rise an event for "up" move
if(!isScrollingUp){
isScrollingUp = true;
}
// Has user scroll down before -> view is smaller than it's default size -> resize it instead of change it position
if(baseLayout.getHeight()<defaultViewHeight){
baseLayout.getLayoutParams().height = baseLayout.getHeight() - (Y - previousFingerPosition);
baseLayout.requestLayout();
}
else {
// Has user scroll enough to "auto close" popup ?
if ((baseLayoutPosition - currentYPosition) > defaultViewHeight / 4) {
closeUpAndDismissDialog(currentYPosition);
return true;
}
//
}
baseLayout.setY(baseLayout.getY() + (Y - previousFingerPosition));
}
// If we scroll down
else{
// First time android rise an event for "down" move
if(!isScrollingDown){
isScrollingDown = true;
}
// Has user scroll enough to "auto close" popup ?
if (Math.abs(baseLayoutPosition - currentYPosition) > defaultViewHeight / 2)
{
closeDownAndDismissDialog(currentYPosition);
return true;
}
// Change base layout size and position (must change position because view anchor is top left corner)
baseLayout.setY(baseLayout.getY() + (Y - previousFingerPosition));
baseLayout.getLayoutParams().height = baseLayout.getHeight() - (Y - previousFingerPosition);
baseLayout.requestLayout();
}
// Update position
previousFingerPosition = Y;
}
break;
}
return true;
}
}
There are two small methods called when user has scroll enough to close popup (ie animate and finish activity) :
public void closeUpAndDismissDialog(int currentPosition){
isClosing = true;
ObjectAnimator positionAnimator = ObjectAnimator.ofFloat(baseLayout, "y", currentPosition, -baseLayout.getHeight());
positionAnimator.setDuration(300);
positionAnimator.addListener(new Animator.AnimatorListener()
{
. . .
#Override
public void onAnimationEnd(Animator animator)
{
finish();
}
. . .
});
positionAnimator.start();
}
public void closeDownAndDismissDialog(int currentPosition){
isClosing = true;
Display display = getWindowManager().getDefaultDisplay();
Point size = new Point();
display.getSize(size);
int screenHeight = size.y;
ObjectAnimator positionAnimator = ObjectAnimator.ofFloat(baseLayout, "y", currentPosition, screenHeight+baseLayout.getHeight());
positionAnimator.setDuration(300);
positionAnimator.addListener(new Animator.AnimatorListener()
{
. . .
#Override
public void onAnimationEnd(Animator animator)
{
finish();
}
. . .
});
positionAnimator.start();
}
With all this code your should be able to start PopupActivity that globally match Facebook popup behaviour. It's just a draft class and a lot of work remains to do : add animations, work on closing parameters and so on...
Screenshots :
I think you can use BottomSheetDialogFragment component from appcompat libarary. Check this article for the information:
https://medium.com/#nullthemall/new-bottomsheet-caab21aff19b#.gpu1l516z
You could also get useful information from
documentaion.
Well, the OP title was asking for a floating activity, but the OP content was looking for a floating comment window that is like facebook comment window.
So, here this will be implemented by a DialogFragment which provide us with the behavior of automatically bouncing the dialog to its original state/window size whenever you swipe up or down. And this behavior is kept when the dialog is swiped a little (exactly swiped a distance less than the half of the original layout size).
The remaining thing is it dismiss this dialog if its size is less than the half of the original window size; in other words, if it's swiped greater than the half of the original layout size. This part is adjusted from the accepted answer by changing the only the Y position of the root layout of the dialog window, and without changing the size of the window as this provided weird resizing behavior.
First create this style to have a transparent background to the dialog window:
<style name="NoBackgroundDialogTheme" parent="Theme.AppCompat.Light.Dialog">
<item name="android:windowBackground">#null</item>
</style>
This style will be applied in the DialogFragment by overriding getTheme() method.
And here is the customized DialogFragment:
class MyDialogFragment : DialogFragment(), View.OnTouchListener {
private var rootLayoutY: Int = 0
private val rootLayout by lazy {
requireView().findViewById<ConstraintLayout>(R.id.dialog_root)
}
private var oldY = 0
private var baseLayoutPosition = 0
private var defaultViewHeight = 0
private var isScrollingUp = false
private var isScrollingDown = false
override fun getTheme(): Int {
return R.style.NoBackgroundDialogTheme
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view: View = inflater.inflate(
R.layout.fragment_dialog_facebook_comment, container,
false
)
view.setBackgroundResource(R.drawable.rounded_background)
return view
}
override fun onStart() {
super.onStart()
// Making the dialog full screen
dialog?.window?.setLayout(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
)
}
#SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
rootLayout.setOnTouchListener(this)
rootLayout.viewTreeObserver.addOnGlobalLayoutListener(object :
OnGlobalLayoutListener {
override fun onGlobalLayout() {
rootLayout.viewTreeObserver
.removeOnGlobalLayoutListener(this)
// save default base layout height
defaultViewHeight = rootLayout.height
}
})
}
#SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
// Get finger position on screen
val y = event!!.rawY.toInt()
// Switch on motion event type
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_DOWN -> {
// Init finger and view position
oldY = y
baseLayoutPosition = rootLayout.y.toInt()
}
MotionEvent.ACTION_UP -> {
if (rootLayoutY >= defaultViewHeight / 2) {
dismiss()
return true
}
// If user was doing a scroll up
if (isScrollingUp) {
// Reset baselayout position
rootLayout.y = 0f
// We are not in scrolling up mode anymore
isScrollingUp = false
}
// If user was doing a scroll down
if (isScrollingDown) {
// Reset baselayout position
rootLayout.y = 0f
// Reset base layout size
rootLayout.layoutParams.height = defaultViewHeight
rootLayout.requestLayout()
// We are not in scrolling down mode anymore
isScrollingDown = false
}
}
MotionEvent.ACTION_MOVE -> {
rootLayoutY = abs(rootLayout.y.toInt())
// Change base layout size and position (must change position because view anchor is top left corner)
rootLayout.y = rootLayout.y + (y - oldY)
if (oldY > y) { // scrolling up
if (!isScrollingUp) isScrollingUp = true
} else { // Scrolling down
if (!isScrollingDown) isScrollingDown = true
}
// Update y position
oldY = y
}
}
return true
}
}
In my case, the root layout of the dialog is a ConstraintLayout.

OnTouchListener on invisible view

so all I want to do is to touch on a certain part of the screen where an invisible view is hidden.
After that touch the view should fade In.
The Problem: The Listener isn't triggered whe I touch the view.
Things I've tried: view.setImageAlpha(0), view.setAlpha(0), view.setVisibility(View.INVISIBLE)
When I set alpha=1 the listener works fine.
Thanks in advance.
Edit:
Thanks to #Viswanath L for the great solution which he mentioned in his post as second workaround.
I just wanted to give a little code example for that in case someone doesn't know how this works:
What I want: I want to touch the right corner of the screen and at this spot, a view(myView) should fade in.
fadeInListener = new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
System.out.println("TRIGGERED");
//The screen size here is 1080x1920(vertical)
if (event.getRawX() > 800 && event.getRawY() > 1640) {
final AlphaAnimation fadeIn = new AlphaAnimation(0, 1);
fadeIn.setDuration(500);
fadeIn.setFillAfter(true);
myView.startAnimation(fadeIn);
}
}
return false;
}
};
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
layout.setOnTouchListener(fadeInListener);
}
The layout is:
this.layout = (RelativeLayout) findViewById(R.id.layout);
And myView should be placed in the right corner and what's very important:
myView.setVisibility(View.Gone);
Hope this helps.
INVISBLE means you are trying to add a listener to a view which is not there. You can add the listener to a visible view only.
WORKAROUND
1) Try to make a dummy view which is visible but having the same color as background.
2) Try to set the listener for parent and check the position (whether the position does
belongs to INVISIBLE view).
onTouchListener does not trigger for an invisible View. You can make 2 exactly the same views one under another the one below having the map. It will be visible and so it can be touched but it will be obscured by the view above like they do here: http://blahti.wordpress.com/2012/06/26/images-with-clickable-areas/
Try this in your xml:
<View
android:id="#+id/my_inv_view"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#color/blue"
/>
<View
android:id="#+id/my_touch_view"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
And set up the click listener like this:
final View mTouchView = findViewById(R.id.my_touch_view);
final View mInvView = findViewById(R.id.my_inv_view);
mInvView.setVisibility(View.INVISIBLE);
mTouchView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
mInvView.setVisibility(View.VISIBLE);
}
});

How I can increase the touch area for controls (ToggleButton)?

I have a ToggleButton. I want to have more place for click on this button. If I add layout_width, layout_height etc. the image looks bad. I also tried using android:padding but it did not help me.
It is needed for the convenience of users.
Easy solution : If you have image for the button, then create transparent area around it (i.e. for touch area).
The grey area can be transparent to increase the touch area.
Or use TouchDelegate
You can also increase touch area by setting touch delegates Android Developer Training Blog Post
public class MainActivity extends Activity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Get the parent view
View parentView = findViewById(R.id.parent_layout);
parentView.post(new Runnable() {
// Post in the parent's message queue to make sure the parent
// lays out its children before you call getHitRect()
#Override
public void run() {
// The bounds for the delegate view (an ImageButton
// in this example)
Rect delegateArea = new Rect();
ImageButton myButton = (ImageButton) findViewById(R.id.button);
myButton.setEnabled(true);
myButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
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 should receive motion
// events.
TouchDelegate touchDelegate = new TouchDelegate(delegateArea,
myButton);
// Sets the TouchDelegate on the parent view, such that touches
// within the touch delegate bounds are routed to the child.
if (View.class.isInstance(myButton.getParent())) {
((View) myButton.getParent()).setTouchDelegate(touchDelegate);
}
}
});
}
}
Use TouchDelegate for your ToggleButton as ammar26 have commented you.
Or
Try this:
Make one parent layout like LinearLayout or RelativeLayout that cover the ToggleButton. And now put margin to that Parent layout.
Now, on click of that Parent layout do action for the toggle button.
Hope it will help you to increase touch area for your view.
Happy Coding.
Instead of putting the touch event in button put it in the layout containg the only the button..and fix the size of the layout as ur wish
increase the values of android:padding:
<SeekBar android:id="#+id/seek" android:layout_width="0dip"
android:layout_height="wrap_content" android:layout_weight="1"
android:paddingTop="5dp" android:paddingBottom="5dp"
android:progressDrawable="#drawable/green_scrubber_progress_horizontal_holo_ligh‌​t"
android:thumb="#drawable/thumb" />
Take the margin (place of padding) of your Button from its parent layout and then perform opration on your Layout
mLayout.setonTouchListener(View.onTouchListener{
// here your working code
});

How To Use Multiple TouchDelegate

i have two ImageButtons, each inside a RelativeLayout and these two RelativeLayouts are in another RelativeLayout, i want to set TouchDelegate for each ImageButton. If normally i add TouchDelegate to each ImageButton and it's parent RelativeLayout then just one ImageButton works properly, Another one doesn't extend it's clicking area. So PLease help me on how to use TouchDelegate in both ImageButtons. If it's not possible then what can be a effective way to extend the clicking area of a view? Thanks in advance ........
Here is my xml code:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout android:id="#+id/FrameContainer"
android:layout_width="fill_parent" android:layout_height="fill_parent"
xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout android:layout_alignParentLeft="true"
android:id="#+id/relativeLayout3" android:layout_width="wrap_content"
android:layout_height="wrap_content">
<RelativeLayout android:layout_alignParentLeft="true"
android:id="#+id/relativeLayout1" android:layout_width="113dip"
android:layout_height="25dip">
<ImageButton android:id="#+id/tutorial1"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:background="#null" android:src="#drawable/tutorial" />
</RelativeLayout>
<RelativeLayout android:layout_alignParentLeft="true"
android:id="#+id/relativeLayout2" android:layout_width="113dip"
android:layout_height="25dip" android:layout_marginLeft="100dip">
<ImageButton android:id="#+id/tutorial2"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:background="#null" android:src="#drawable/tutorial"
android:layout_marginLeft="50dip" />
</RelativeLayout>
</RelativeLayout>
My Activity class :
import android.app.Activity;
import android.graphics.Rect;
import android.os.Bundle;
import android.view.TouchDelegate;
import android.view.View;
import android.widget.ImageButton;
import android.widget.Toast;
public class TestTouchDelegate extends Activity {
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
View mParent1 = findViewById(R.id.relativeLayout1);
mParent1.post(new Runnable() {
#Override
public void run() {
Rect bounds1 = new Rect();
ImageButton mTutorialButton1 = (ImageButton) findViewById(R.id.tutorial1);
mTutorialButton1.setEnabled(true);
mTutorialButton1.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
Toast.makeText(TestTouchDelegate.this, "Test TouchDelegate 1", Toast.LENGTH_SHORT).show();
}
});
mTutorialButton1.getHitRect(bounds1);
bounds1.right += 50;
TouchDelegate touchDelegate1 = new TouchDelegate(bounds1, mTutorialButton1);
if (View.class.isInstance(mTutorialButton1.getParent())) {
((View) mTutorialButton1.getParent()).setTouchDelegate(touchDelegate1);
}
}
});
//View mParent = findViewById(R.id.FrameContainer);
View mParent2 = findViewById(R.id.relativeLayout2);
mParent2.post(new Runnable() {
#Override
public void run() {
Rect bounds2 = new Rect();
ImageButton mTutorialButton2 = (ImageButton) findViewById(R.id.tutorial2);
mTutorialButton2.setEnabled(true);
mTutorialButton2.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
Toast.makeText(TestTouchDelegate.this, "Test TouchDelegate 2", Toast.LENGTH_SHORT).show();
}
});
mTutorialButton2.getHitRect(bounds2);
bounds2.left += 50;
TouchDelegate touchDelegate2 = new TouchDelegate(bounds2, mTutorialButton2);
if (View.class.isInstance(mTutorialButton2.getParent())) {
((View) mTutorialButton2.getParent()).setTouchDelegate(touchDelegate2);
}
}
});
}
}
You can use composite pattern to be able to add more than one TouchDelegate to the View. Steps:
Create TouchDelegateComposite (no matter what view you'll pass as an
argument, it's used just to get the Context)
Create necessary TouchDelegates and add them to composite
Add composite to view as they recommend here (via view.post(new Runnable))
public class TouchDelegateComposite extends TouchDelegate {
private final List<TouchDelegate> delegates = new ArrayList<TouchDelegate>();
private static final Rect emptyRect = new Rect();
public TouchDelegateComposite(View view) {
super(emptyRect, view);
}
public void addDelegate(TouchDelegate delegate) {
if (delegate != null) {
delegates.add(delegate);
}
}
#Override
public boolean onTouchEvent(MotionEvent event) {
boolean res = false;
float x = event.getX();
float y = event.getY();
for (TouchDelegate delegate : delegates) {
event.setLocation(x, y);
res = delegate.onTouchEvent(event) || res;
}
return res;
}
}
There is only supposed to be one touch delegate for each view. The documentation for getTouchDelegate() in the View class reads:
"Gets the TouchDelegate for this View."
There is only to be one TouchDelegate. To use only one TouchDelegate per view, you can wrap each touchable view within a view with dimensions reflecting what you would like to be touchable. An android developer at square gives an example of how you can do this for multiple Views using just one static method (http://www.youtube.com/watch?v=jF6Ad4GYjRU&t=37m4s):
public static void expandTouchArea(final View bigView, final View smallView, final int extraPadding) {
bigView.post(new Runnable() {
#Override
public void run() {
Rect rect = new Rect();
smallView.getHitRect(rect);
rect.top -= extraPadding;
rect.left -= extraPadding;
rect.right += extraPadding;
rect.bottom += extraPadding;
bigView.setTouchDelegate(new TouchDelegate(rect, smallView));
}
});
}
Let's say that you do not want to clutter your view hierarchy. There are two other options I can think of. You can define the bounds of what is touchable inside the touchable view and make sure to pass all touchevents to that child view from respective parent views. Or you can override getHitRect() for the touchable view. The former will quickly clutter your code and make it difficult to understand, so the latter is the better way forward. You want to go with overriding getHitRect.
Where mPadding is the amount of extra area you want to be touchable around your view, you could use something like the following:
#Override
public void getHitRect(Rect outRect) {
outRect.set(getLeft() - mPadding, getTop() - mPadding, getRight() + mPadding, getTop() + mPadding);
}
If you use code like the above you'll have to consider what touchable views are nearby. The touchable area of the View that is highest on the stack could overlap on top of another View.
Another similar option would be to just change the padding of the touchable view. I dislike this as a solution because it can become difficult to keep track of how Views are being resized.
Kotlin version of #need1milliondollars's answer:
class TouchDelegateComposite(view: View) : TouchDelegate(Rect(), view) {
private val delegates: MutableList<TouchDelegate> = ArrayList()
fun addDelegate(delegate: TouchDelegate) {
delegates.add(delegate)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
var res = false
val x = event.x
val y = event.y
for (delegate in delegates) {
event.setLocation(x, y)
res = delegate.onTouchEvent(event) || res
}
return res
}
}
Fully working Kotlin extension function which allows for multiple views to increase their touch target by the same amount:
// Example of usage
parentLayout.increaseHitAreaForViews(views = *arrayOf(myFirstView, mySecondView, myThirdView))
/*
* Use this function if a parent view contains more than one view that
* needs to increase its touch target hit area.
*
* Call this on the parent view
*/
fun View.increaseHitAreaForViews(#DimenRes radiusIncreaseDpRes: Int = R.dimen.touch_target_default_radius_increase, vararg views: View) {
val increasedRadiusPixels = resources.getDimensionPixelSize(radiusIncreaseDpRes)
val touchDelegateComposite = TouchDelegateComposite(this)
post {
views.forEach { view ->
val rect = Rect()
view.getHitRect(rect)
rect.top -= increasedRadiusPixels
rect.left -= increasedRadiusPixels
rect.bottom += increasedRadiusPixels
rect.right += increasedRadiusPixels
touchDelegateComposite.addDelegate(TouchDelegate(rect, view))
}
touchDelegate = touchDelegateComposite
}
}
class TouchDelegateComposite(view: View) : TouchDelegate(Rect(), view) {
private val delegates = mutableListOf<TouchDelegate>()
fun addDelegate(delegate: TouchDelegate) {
delegates.add(delegate)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
var res = false
for (delegate in delegates) {
event.setLocation(event.x, event.y)
res = delegate.onTouchEvent(event) || res
}
return res
}
}
To make your code working you need to decrease left border of bounds2, and not increase it.
bounds2.left -= 50;
After playing around with TouchDelegate, I came to the code below, which works for me all the time on any Android version. The trick is to extend area guarantied after layout is called.
public class ViewUtils {
public static final void extendTouchArea(final View view,
final int padding) {
view.getViewTreeObserver().addOnGlobalLayoutListener(
new OnGlobalLayoutListener() {
#Override
#SuppressWarnings("deprecation")
public void onGlobalLayout() {
final Rect touchArea = new Rect();
view.getHitRect(touchArea);
touchArea.top -= padding;
touchArea.bottom += padding;
touchArea.left -= padding;
touchArea.right += padding;
final TouchDelegate touchDelegate =
new TouchDelegate(touchArea, view);
final View parent = (View) view.getParent();
parent.setTouchDelegate(touchDelegate);
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
});
}
}
this seemed to be working for me http://cyrilmottier.com/2012/02/16/listview-tips-tricks-5-enlarged-touchable-areas/
Ok i guess nobody provides the real answer to make solution of it and make it easy.Lately i had same issue and reading all above i just had no clue how just to make it work.But finally i did it.First thing to keep in your mind!Lets pretend you have one whole layout which is holding your two small buttons which area must be expanded,so you MUST make another layout inside your main layout and put another button to your newly created layout so in that case with static method you can give touch delegate to 2 buttons at the same time.Now more deeply and step by step into code!
first you surely just find the view of your MAINLAYOUT and Button like this.(this layout will hold our first button)
RelativeLayout mymainlayout = (RelativeLayout)findViewById(R.id.mymainlayout)
Button mybutoninthislayout = (Button)findViewById(R.id.mybutton)
ok we done finding the main layout and its button view which will hold everything its just our onCreate() displaying layout but you have to find in case to use it later.Ok what next?We create another RelativeLayout inside our main layout which width and height is on your taste(this newly created layout will hold our second button)
RelativeLayout myinnerlayout = (RelativeLayout)findViewById(R.id.myinnerlayout)
Button mybuttoninsideinnerlayout = (Button)findViewById(R.id.mysecondbutton)
ok we done finding views so we can now just copy and paste the code of our guy who firstly answered your question.Just copy that code inside your main activity.
public static void expandTouchArea(final View bigView, final View smallView, final int extraPadding) {
bigView.post(new Runnable() {
#Override
public void run() {
Rect rect = new Rect();
smallView.getHitRect(rect);
rect.top -= extraPadding;
rect.left -= extraPadding;
rect.right += extraPadding;
rect.bottom += extraPadding;
bigView.setTouchDelegate(new TouchDelegate(rect, smallView));
}
});
}
Now how to use this method and make it work?here is how!
on your onCreate() method paste the next code snippet
expandTouchArea(mymainlayout,mybutoninthislayout,60);
expandTouchArea(myinnerlayout, mybuttoninsideinnerlayout,60);
Explanation on what we did in this snipped.We took our created static method named expandTouchArea() and gave 3 arguments.
1st argument-The layout which holds the button which area must be expanded
2nd argument - actual button to expand the area of it
3rd argument - the area in pixels of how much we want the button area to be expanded!
ENJOY!
I had the same issue: Trying to add multiple TouchDelegates for different LinearLayouts that route touch events to separate Switches respectively, in one layout.
For details please refer to this question asked by me and my answer.
What I found is: Once I enclose the LinearLayouts each by another LinearLayout, respectively, the second (end every other successive) TouchDelegate starts to work as expected.
So this might help the OP to create a working solution to his problem.
I don't have a satisfying explanation on why it behaves like this, though.
I copy the code of TouchDelegate and made some alter. Now it can support multi Views regardless of whether thoese views had common parents
class MyTouchDelegate: TouchDelegate {
private var mDelegateViews = ArrayList<View>()
private var mBoundses = ArrayList<Rect>()
private var mSlopBoundses = ArrayList<Rect>()
private var mDelegateTargeted: Boolean = false
val ABOVE = 1
val BELOW = 2
val TO_LEFT = 4
val TO_RIGHT = 8
private var mSlop: Int = 0
constructor(context: Context): super(Rect(), View(context)) {
mSlop = ViewConfiguration.get(context).scaledTouchSlop
}
fun addTouchDelegate(delegateView: View, bounds: Rect) {
val slopBounds = Rect(bounds)
slopBounds.inset(-mSlop, -mSlop)
mDelegateViews.add(delegateView)
mSlopBoundses.add(slopBounds)
mBoundses.add(Rect(bounds))
}
var targetIndex = -1
override fun onTouchEvent(event: MotionEvent): Boolean {
val x = event.x.toInt()
val y = event.y.toInt()
var sendToDelegate = false
var hit = true
var handled = false
when (event.action) {
MotionEvent.ACTION_DOWN -> {
targetIndex = -1
for ((index, item) in mBoundses.withIndex()) {
if (item.contains(x, y)) {
mDelegateTargeted = true
targetIndex = index
sendToDelegate = true
}
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_MOVE -> {
sendToDelegate = mDelegateTargeted
if (sendToDelegate) {
val slopBounds = mSlopBoundses[targetIndex]
if (!slopBounds.contains(x, y)) {
hit = false
}
}
}
MotionEvent.ACTION_CANCEL -> {
sendToDelegate = mDelegateTargeted
mDelegateTargeted = false
}
}
if (sendToDelegate) {
val delegateView = mDelegateViews[targetIndex]
if (hit) {
// Offset event coordinates to be inside the target view
event.setLocation((delegateView.width / 2).toFloat(), (delegateView.height / 2).toFloat())
} else {
// Offset event coordinates to be outside the target view (in case it does
// something like tracking pressed state)
val slop = mSlop
event.setLocation((-(slop * 2)).toFloat(), (-(slop * 2)).toFloat())
}
handled = delegateView.dispatchTouchEvent(event)
}
return handled
}
}
use it like this
fun expandTouchArea(viewList: List<View>, touchSlop: Int) {
val rect = Rect()
viewList.forEach {
it.getHitRect(rect)
if (rect.left == rect.right && rect.top == rect.bottom) {
postDelay(Runnable { expandTouchArea(viewList, touchSlop) }, 200)
return
}
rect.top -= touchSlop
rect.left -= touchSlop
rect.right += touchSlop
rect.bottom += touchSlop
val parent = it.parent as? View
if (parent != null) {
val parentDelegate = parent.touchDelegate
if (parentDelegate != null) {
(parentDelegate as? MyTouchDelegate)?.addTouchDelegate(it, rect)
} else {
val touchDelegate = MyTouchDelegate(this)
touchDelegate.addTouchDelegate(it, rect)
parent.touchDelegate = touchDelegate
}
}
}
}
I implemented a simple solution from the link that Brendan Weinstein listed above - the static method was incredibly clean and tidy compared to all other solutions. Padding for me simply doesnt work.
My use case was increasing the touch area of 2 small buttons to improve UX.
I have a MyUserInterfaceManager class, where i insert the static function from the youtube video;
public static void changeViewsTouchArea(final View newTouchArea,
final View viewToChange) {
newTouchArea.post(new Runnable() {
#Override
public void run() {
Rect rect = new Rect(0,0, newTouchArea.getWidth(), newTouchArea.getHeight());
newTouchArea.setTouchDelegate(new TouchDelegate(rect, viewToChange));
}
});
}
Then in XML i have the following code (per imagebutton);
<!-- constrain touch area to button / view!-->
<FrameLayout
android:id="#+id/btn_a_touch_area"
android:layout_width="#dimen/large_touch_area"
android:layout_height="#dimen/large_touch_area"
/>
<ImageButton
android:id="#+id/btn_id_here"
android:layout_width="#dimen/button_size"
android:layout_height="#dimen/button_size" />
The in the onCreateView(...) method of my fragment i called the method per View that needs the touch area modified (after binding both Views!);
MyUserInterfaceManager.changeViewsTouchArea(buttonATouchArea, buttonA);
buttonA.setOnClickListener(v -> {
// add event code here
});
This solution means i can explicitly design and see the touch area in layout files - a must have to ensure im not getting too close to other View touch areas (compared to the "calculate and add pixels" methods recommended by google and others).

Categories

Resources