Apply animation sequentially to multiple views - android

I have an activity with 3 views (buttonViews) in a vertical linear layout. I am generating (inflating) these views dynamically. I want to apply an animation such that, on activity start, the first buttons slide in -> 100 ms delay -> second button slide in -> 100 ms delay -> Third button slide in.
Attempt
I tried implementing it in this way:
private void setMainButtons() {
ArrayList<String> dashboardTitles = DashboardUtils.getDashboardTitles();
ArrayList<Integer> dashboardIcons = DashboardUtils.getDashboardIcons();
final ViewGroup root = findViewById(R.id.button_container);
for (int i = 0; i < (dashboardTitles.size() < dashboardIcons.size() ? dashboardTitles.size() : dashboardIcons.size()); i++){
final View buttonView = DashboardButtonInflater.getDashboardButton(root, dashboardTitles.get(i), dashboardIcons.get(i), this);
if (buttonView == null) continue;
buttonView.setOnClickListener(this);
root.addView(buttonView);
animateBottomToTop(buttonView, (long) (i*50)); // Calling method to animate buttonView
}
}
//The function that adds animation to buttonView, with a delay.
private void animateBottomToTop(final View buttonView,long delay) {
AnimationSet animationSet = new AnimationSet(false);
animationSet.addAnimation(bottomToTop);
animationSet.addAnimation(fadeIn);
animationSet.setStartOffset(delay);
buttonView.setAnimation(animationSet);
}
Result:
The above method waits for the total delay of all the views and at the end, aminates all the views together. I can guess the culprit here is the thread. The dealy is actually stopping the UI thread from doing any animation. I could be wrong though.
I also tried running the animation code inside
new Thread(new Runnable(){...}).run()
but that didn't work either.
Expectations:
Can somebody help me achieve the one-by-one animation on buttonView? Thank you.

Animations are statefull objects, you should not use the same instance multiple times simultaneously. In your case the bottomToTop and fadeIn animations are shared between the animation sets. When the set starts (initialize() is called) it will set the start offset of its children.
For example the method could look like :
//The function that adds animation to buttonView, with a delay.
private void animateBottomToTop(final View buttonView,long delay) {
AnimationSet animationSet = new AnimationSet(false);
// create new instances of the animations each time
animationSet.addAnimation(createBottomToTop());
animationSet.addAnimation(createFadeIn());
animationSet.setStartOffset(delay);
buttonView.setAnimation(animationSet);
}

The problem might be easily solved with Transitions API. Having declared a root layout with this xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/content_frame"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"/>
Then inside activity:
class MainActivity : AppCompatActivity() {
lateinit var content: LinearLayout
private var counter = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
content = findViewById(R.id.content_frame)
// wait this view to be laid out and only then start adding and animating views
content.post { addNextChild() }
}
private fun addNextChild() {
// terminal condition
if (counter >= 3) return
++counter
val button = createButton()
val slide = Slide()
slide.duration = 500
slide.startDelay = 100
slide.addListener(object : TransitionListenerAdapter() {
override fun onTransitionEnd(transition: Transition) {
addNextChild()
}
})
TransitionManager.beginDelayedTransition(content, slide)
content.addView(button)
}
private fun createButton(): Button {
val button = Button(this)
button.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
button.text = "button"
return button
}
}
This chunk of code will result in following output:
You can adjust animation and delay times respectively.
If you want following behavior:
Then you can use following code:
class MainActivity : AppCompatActivity() {
lateinit var content: LinearLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
content = findViewById(R.id.content_frame)
content.post { addChildren() }
}
private fun addChildren() {
val button1 = createButton()
val button2 = createButton()
val button3 = createButton()
val slide1 = Slide()
slide1.duration = 500
slide1.addTarget(button1)
val slide2 = Slide()
slide2.duration = 500
slide2.startDelay = 150
slide2.addTarget(button2)
val slide3 = Slide()
slide3.duration = 500
slide3.startDelay = 300
slide3.addTarget(button3)
val set = TransitionSet()
set.addTransition(slide1)
set.addTransition(slide2)
set.addTransition(slide3)
TransitionManager.beginDelayedTransition(content, set)
content.addView(button1)
content.addView(button2)
content.addView(button3)
}
private fun createButton(): Button {
val button = Button(this)
button.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
button.text = "button"
return button
}
}

Create method, which will accept Any number of Animation to invoke one after another. Just as example.
private void playOneAfterAnother(#NonNull Queue<Animation> anims) {
final Animation next = anims.poll();
/* You can set any other paramters,
like delay, for each next Playing view, if any of course */
next.addListener(new AnimationListener() {
#Override
public void onAnimationEnd(Animator a) {
if (!anim.isEmpty()) {
playOneAfterAnother(anims);
}
}
#Override
public void onAnimationStart(Animator a) {
}
#Override
public void onAnimationCancel(Animator a) {
}
#Override
public void onAnimationRepeat(Animator a) {
}
});
next.play();
}
Or with delay for animations, it's easy too.
private void playOneAfterAnother(#NonNull Queue<Animation> anims,
long offsetBetween, int nextIndex) {
final Animation next = anims.poll();
/* You can set any other paramters,
like delay, for each next Playing view, if any of course */
next.setStartOffset(offsetBetween * nextIndex);
next.play();
if (!anim.isEmpty()) {
playOneAfterAnother(anims,
offsetBetween, nextIndex +1);
}
}

Probably, what you need to use is AnimatorSet instead of AnimationSet. The AnimatorSet API allows you to choreograph animations in two ways:
1. PlaySequentially
2. PlayTogether
using the apis:
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playSequentially(anim1, anim2, anim3, ...);
animatorSet.playTogether(anim1, anim2, anim3, ...);
You can further add delays to your animation using
animatorSet.setStartDelay();
Visit the complete API docs here https://developer.android.com/reference/android/animation/AnimatorSet
Hope this helps!

I Use This Code And its work correctly
Hope to help you
// first put your views in an array with name arrayViews
final int[] time = {0};
for (int i = 0; i < arrayViews.size(); i++) {
Animation zoom = AnimationUtils.loadAnimation(this, R.anim.fade_in);
zoom.setDuration(250);
zoom.setStartOffset(time[0] += 250);
arrayViews.get(i).startAnimation(zoom);
arrayViews.get(i).setVisibility(View.VISIBLE);
}

Related

Play animation to each view sequentially with a method call

The fragment has several CardViews. I've made an utility class that contains the next method:
public void applyAnimationToEachView(#NonNull Collection<View> views,
#NonNull AnimationSet animationSet,
long offset,
boolean offsetSequentially) {
int i = 0;
for (View view: views) {
view.setAnimation(animationSet);
if (offsetSequentially)
view.getAnimation().setStartOffset(offset * i);
else view.getAnimation().setStartOffset(offset);
view.getAnimation().start();
i++;
}
}
I call the method this way:
override fun onHiddenChanged(hidden: Boolean) {
if (!hidden) AnimationManager().applyAnimationToEachView(
visibleCards as Collection<View>,
getAnimationSet(),
100,
true)
}
However, when I call the method, it sums up the offsets and shows the animation inconsequentially. When I do the same thing inside of the fragment, it works as intended:
var i : Long = 0
if (!hidden)
for (cardView in visibleCards) {
cardView.animation = getAnimationSet()
cardView.animation.startOffset = 100 * i
i = i.inc()
}
What's the reason behind this behavior? And can I somehow hide this functionality inside of my utility class?
That's because you're passing the same instance of AnimationSet to each View and then retrieve it with view.getAnimation(). Thus its start offset increases on each loop iteration. In order to make it work, you'll need to create a new Animation for each View.

android- simple fade out and fade in animation for viewflipper

I'm new to android and I don't know a lot about android animation .
I've a viewflipper and I want to animate between images inside it .
This is the code :
runnable = new Runnable() {
public void run() {
handler.postDelayed(runnable, 3000);
imageViewFlipper.setInAnimation(fadeIn);
imageViewFlipper.setOutAnimation(fadeOut);
imageViewFlipper.showNext();
}
};
handler = new Handler();
handler.postDelayed(runnable, 500);
}
The 2 animated file are not good , they animate very badly .
I just need a code to fade out the front image and fade in the next image and do the same for all images inside it.
Could anyone help me out ?
thank you
Simple using Kotlin.
First, set the animations:
private fun setAnimations() {
// in anim
val inAnim = AlphaAnimation(0f, 1f)
inAnim.duration = 600
flipperView.inAnimation = inAnim
// out anim
val outAnim = AlphaAnimation(1f, 0f)
outAnim.duration = 600
flipperView.outAnimation = outAnim
}
And to flip (between two views) just call this function:
fun onSendClick(view: View) {
viewFlipper.displayedChild = if(flipperView.displayedChild == 0) 1 else 0
}

removeView doesn't remove View immediately and destroys Layout

I'm trying to dynamically remove some components in a LinearLayout in Android. The logic I have implemented is: If someone clicks a Button in my (Sidebar)Fragment I start an Animation that moves the clicked Button up and the Other Buttons out of the left side of the Display. The Animation triggers an AnimationListener that, after the Animation is complete, iterates over all Buttons in the Layout and removes all non clicked Buttons.
The Problem is: if I disable the Fade Out Animation and step in with the debugger i can see, that after removeView() the Views are still there.
The even bigger Problem is that my Button that should still be Visible (the clicked one) disappears and only shows up again, if I set X and Y Position manually at a later Time.
Is there any way i can "fix" the Button while removal and when does removeView() actually remove the View/update the Layout?
I already tried to remove all Views and then add the Button again with the same result.
Some snippets to clear things up a Bit:
//Start Animation over all Components
//Commented out the move outside animation to ensure the Layout doesn't get streched in this process
for (mComponentIterator = mLayout.getChildCount() - 1; mComponentIterator >= 0; mComponentIterator--) {
final ImageTextButton currentButton = (ImageTextButton) mLayout
.getChildAt(mComponentIterator);
ObjectAnimator buttonAnimation;
if (!currentButton.equals(pClickedButton)) {
// buttonAnimation = ObjectAnimator.ofFloat(currentButton, View.X, POSITION_OUTSIDE);
} else {
// buttonAnimation = ObjectAnimator.ofFloat(currentButton, View.Y, POSITION_UPPER);
animations.add( ObjectAnimator.ofFloat(currentButton, View.Y, POSITION_UPPER));
currentButton.setFocused(true);
mSelectedButton = currentButton;
}
// animations.add(buttonAnimation);
}
buttonAnimations.playTogether(animations);
buttonAnimations.setDuration(mShortAnimationDuration);
buttonAnimations.setInterpolator(new DecelerateInterpolator());
buttonAnimations.addListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationEnd(Animator arg0) {
for (mComponentIterator = mLayout.getChildCount() - 1; mComponentIterator >= 0; mComponentIterator--) {
final ImageTextButton currentButton = (ImageTextButton) mLayout
.getChildAt(mComponentIterator);
if (currentButton.equals(mSelectedButton)) {
if (mCurrentFragment != null) {
//This changes the Layout in another
mListener.onChangeLayoutRequest(R.id.maincontent, mCurrentFragment);
}
} else {
mLayout.removeView(currentButton);
}
}
}
});
buttonAnimations.start();
//Callback when Activity is ready
mCurrentFragment.SetOnActivityCreatedListener(new OnActivityCreatedListener() {
#Override
public void onActivityCreated(Activity activity) {
Drawable grayMenuBackground = getActivity().getResources().getDrawable(
R.drawable.bg_menuitem);
Drawable coloredMenuBackground = grayMenuBackground.getConstantState()
.newDrawable();
coloredMenuBackground.setColorFilter(mSelectedMenuColor,
PorterDuff.Mode.SRC_ATOP);
LinearLayout.LayoutParams rightGravityParams = new LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
rightGravityParams.gravity = Gravity.TOP;
rightGravityParams.weight = 0;
Drawable seperator = getResources().getDrawable(R.drawable.seperator_menu);
seperator.setBounds(new Rect(0, 0, mSelectedButton.getWidth(), 1));
int insertIndex = 1;
for (Button menuButton : mCurrentFragment.getMenuItems()) {
//Some Button setupcode
mLayout.addView(menuButton, insertIndex++);
}
//Here the Button gets visible again
mSelectedButton.setLayoutParams(rightGravityParams);
mSelectedButton.setX(POSITION_LEFT);
mSelectedButton.setY(POSITION_UPPER);
}
});
Im stuck at this problem for two days now. Currently the Buttons Animate in the right manner, then, right after the animation has finished, all Buttons dissapear(also the Clicked Button). Then, after 1/2sec. the other fragment needs to load, the clicked button appears again
PS: I already scheduled the removal with ''mLayout.post()'' with the same result
What happens if you simply do:
button.setVisibility(View.GONE) ;
Added some comments along your code.
//Start Animation over all Components
//Commented out the move outside animation to ensure the Layout doesn't get streched in this process
for (mComponentIterator = mLayout.getChildCount() - 1; mComponentIterator >= 0; mComponentIterator--) {
final ImageTextButton currentButton = (ImageTextButton) mLayout
.getChildAt(mComponentIterator);
ObjectAnimator buttonAnimation;
if (!currentButton.equals(pClickedButton)) {
// buttonAnimation = ObjectAnimator.ofFloat(currentButton, View.X, POSITION_OUTSIDE);
} else {
// buttonAnimation = ObjectAnimator.ofFloat(currentButton, View.Y, POSITION_UPPER);
/*
* You say that you want to keep the clicked button, yet you
* add it to the animations?
*/
animations.add( ObjectAnimator.ofFloat(currentButton, View.Y, POSITION_UPPER));
currentButton.setFocused(true);
mSelectedButton = currentButton;
}
// animations.add(buttonAnimation);
}
buttonAnimations.playTogether(animations);
buttonAnimations.setDuration(mShortAnimationDuration);
buttonAnimations.setInterpolator(new DecelerateInterpolator());
buttonAnimations.addListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationEnd(Animator arg0) {
for (mComponentIterator = mLayout.getChildCount() - 1; mComponentIterator >= 0; mComponentIterator--) {
final ImageTextButton currentButton = (ImageTextButton) mLayout
.getChildAt(mComponentIterator);
if (currentButton.equals(mSelectedButton)) {
if (mCurrentFragment != null) {
//This changes the Layout in another
/*
* What exactly does this do and why does it have to
* be done (numberOfButtons - 1) times?
*/
mListener.onChangeLayoutRequest(R.id.maincontent, mCurrentFragment);
}
} else {
/*
* This does indeed only remove the non selected buttons
* I think, but the animation is still applied to
* the selcted button.
* So it makes sense that you have to reset X and Y
* to see it again.
*/
mLayout.removeView(currentButton);
}
}
}
});
buttonAnimations.start();
//Callback when Activity is ready
mCurrentFragment.SetOnActivityCreatedListener(new OnActivityCreatedListener() {
#Override
public void onActivityCreated(Activity activity) {
Drawable grayMenuBackground = getActivity().getResources().getDrawable(
R.drawable.bg_menuitem);
Drawable coloredMenuBackground = grayMenuBackground.getConstantState()
.newDrawable();
coloredMenuBackground.setColorFilter(mSelectedMenuColor,
PorterDuff.Mode.SRC_ATOP);
LinearLayout.LayoutParams rightGravityParams = new LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
rightGravityParams.gravity = Gravity.TOP;
rightGravityParams.weight = 0;
Drawable seperator = getResources().getDrawable(R.drawable.seperator_menu);
seperator.setBounds(new Rect(0, 0, mSelectedButton.getWidth(), 1));
int insertIndex = 1;
for (Button menuButton : mCurrentFragment.getMenuItems()) {
//Some Button setupcode
mLayout.addView(menuButton, insertIndex++);
}
//Here the Button gets visible again
mSelectedButton.setLayoutParams(rightGravityParams);
mSelectedButton.setX(POSITION_LEFT);
mSelectedButton.setY(POSITION_UPPER);
}
});

force layout to refresh/repaint android?

I want to change position of layout and after 75ms return it to first position to make a movement and that is my code:
for(int i = 0; i < l1.getChildCount(); i++) {
linear = (LinearLayout) findViewById(l1.getChildAt(i).getId());
LayoutParams params = new LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,RelativeLayout.LayoutParams.WRAP_CONTENT);
params.bottomMargin = 10;
linear.setLayoutParams(params);
SystemClock.sleep(75);
}
The problem is the app is stop for 750ms and don't do anything. I tried invalidate() , refreshDrawableState(), requestLayout(), postInvalidate(), and try to call onResume(), onRestart(), onPause() .
Maybe you need:
linear.invalidate();
linear.requestLayout();
after making the layout changes.
EDIT:
Run the code on a different thread:
new Thread() {
#Override
public void run() {
<your code here>
}
}.start();
And whenever you need to update the UI from that thread use:
activity.runOnUiThread(new Runnable() {
#Override
public void run() {
<code to change UI>
}
});
After several hours of testing, I found the solution about updating a view if you made operation with these views like adding children, visibility, rotation, etc.
We need to force update the view with the below methods.
linearSliderDots.post {
// here linearSliderDots is a linear layout &
// I made add & remove view option on runtime
linearSliderDots.invalidate()
linearSliderDots.requestLayout()
}
You should try using an ValueAnimator (Or object animator), the below code is in kotlin but same logic would be applied for java:
val childCount = someView.childCount
val animators = mutableListOf<ValueAnimator>()
for (i in 0..childCount) {
val child = (someView.getChildAt(i))
val animator = ValueAnimator.ofInt(0, 75)
animator.addUpdateListener {
val curValue = it.animatedValue as Int
(child.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin = curValue
child.requestLayout()
}
animator.duration = 75
animator.startDelay = 75L * i
animators.add(animator)
}
animators.forEach { animator ->
animator.start()
}
Basically you create a bunch of animators that have start delay proportionate to the number of children, so as soon as one animation ends, the new one starts
ActivityName.this.runOnUiThread(new Runnable() {
#Override
public void run() {
<code to change UI>
}
});

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