Reputation: 2140
I'm just starting to play with MotionLayout
. I have defined an activity layout using MotionLayout
that uses a MotionScene
to hide and show a view.
The MotionScene
transition looks like this:
<Transition
app:constraintSetStart="@id/collapsed"
app:constraintSetEnd="@id/expanded">
<OnClick app:target="@id/nextButton" />
</Transition>
Troublie is, nothing happens when I programmatically add a ClickListener to the button:
nextButton.setOnClickListener {
//do some stuff
}
This listener is completely ignored but the transition (view expanding/collapsing) is triggered with every click. I have seen where someone extends MotionLayout
to handle click events but it seems like there might be an easier way to add another click listener for the button.
Question 1: Is there a way to add a ClickListener to the target of an OnClick in a MotionLayout transition?
Question 2: Is there a way to make the transition a one time only event? The desired result is that if the view is collapsed when the button is clicked, then the view expands, but if it's already expanded then it stays expanded.
Lastly, I'm using the namespace "http://schemas.android.com/apk/res-auto"
and the docs clearly state that target
and mode
are attributes for OnClick. But the project won't compile when I use mode
because it can't be found in that namespace.
Question 3: Am I using the correct namespace?
Upvotes: 25
Views: 15472
Reputation: 33
I got the problem yesterday and just now it solved and all I do is just add a View
tag inside of the MotionLayout
tag, then give it an onClick
attribute.
here is the preview
<androidx.constraintlayout.motion.widget.MotionLayout
android:id="@+id/buttonMotion"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_gravity="center"
app:layoutDescription="@xml/circle_to_square">
<androidx.constraintlayout.utils.widget.ImageFilterView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/puthPic"
android:scaleType="centerCrop"
android:src="@drawable/red"
android:onClick="toggleBroadcasting"
/>
<View
android:id="@+id/puthPicView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:onClick="toggleBroadcasting"/>
</androidx.constraintlayout.motion.widget.MotionLayout>
You can see my gist here for further information.
Upvotes: 0
Reputation: 1302
The simplest solution, in my opinion, is the MotionLayout
extension
You can create your own custom MotionLayout
and process touches with some threshold. For example:
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.motion.widget.MotionLayout;
public class CustomMotionLayout extends MotionLayout {
private final static int CLICK_ACTION_THRESHOLD = 5;
private float startX;
private float startY;
public CustomMotionLayout(@NonNull Context context) {
super(context);
}
public CustomMotionLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CustomMotionLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = event.getX();
startY = event.getY();
break;
case MotionEvent.ACTION_UP:
float endX = event.getX();
float endY = event.getY();
if (isAClick(startX, endX, startY, endY)) {
// here we pass the MotionEvent
// to the next touch listener, bypassing
// the MotionLayout touch event handler
return false;
}
break;
}
return super.onTouchEvent(event);
}
private boolean isAClick(float startX, float endX, float startY, float endY) {
float differenceX = Math.abs(startX - endX);
float differenceY = Math.abs(startY - endY);
return !(differenceX > CLICK_ACTION_THRESHOLD || differenceY > CLICK_ACTION_THRESHOLD);
}
}
You can then handle the click on the item like this:
binding.carousel.setAdapter(new Carousel.Adapter() {
@Override
public int count() {
return images.length;
}
@SuppressLint("ClickableViewAccessibility")
@Override
public void populate(View view, int index) {
if (view instanceof ImageView) {
ImageView imageView = (ImageView) view;
imageView.setImageResource(images[index]);
imageView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
Log.d("CLICK", "click"); // <-- click
return false;
}
});
}
}
@Override
public void onNewItem(int index) {}
});
It's not a perfect implementation, just an example, but it doesn't interfere with the animated carousel and handles clicks
Upvotes: 0
Reputation: 1
Here is simple solution:
Just add this fun:
@SuppressLint("ClickableViewAccessibility")
fun View.setOnClick(clickEvent: () -> Unit) {
this.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_UP) {
clickEvent.invoke()
}
false
}
}
This is how you use it:
nextButton.setOnClick {
//Do something
}
Upvotes: 0
Reputation: 5323
In general if you need a callback you probably will want to control the animation yourself. So if you are adding an onClick you should call the transition yourself.
public void onClick(View v) {
((MotionLayout)v.getParent()).transitionToEnd());
// you can decide all the actions and conditionals.
}
The intent was is useful what the developer does not care. hide/reveal of ui elements etc. or for testing before you get to wiring up the callbacks.
Upvotes: 1
Reputation: 41
You can implement MotionLayout.TransitionListener to handler event when transition.
public class LoginActivity extends AppCompatActivity implements MotionLayout.TransitionListener {
private static final String TAG = "LoginActivity";
private FirebaseAuth mAuth;
private LoginLayoutBinding binding;
@SuppressLint("ClickableViewAccessibility")
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = LoginLayoutBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// initialize the FirebaseAuth instance.
mAuth = FirebaseAuth.getInstance();
binding.getRoot().addTransitionListener(this);
}
@Override
public void onStart() {
super.onStart();
// Check if user is signed in (non-null) and update UI accordingly.
FirebaseUser currentUser = mAuth.getCurrentUser();
updateUI(currentUser);
}
private void updateUI(FirebaseUser currentUser) {
hideProgressBar();
if (currentUser != null) {
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
startActivity(intent);
finish();
}
}
private void hideProgressBar() {
binding.progressBar2.setVisibility(View.GONE);
}
private void createAccount(String email, String password) {
mAuth.createUserWithEmailAndPassword(email, password)
.addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
@Override
public void onComplete(@NonNull Task<AuthResult> task) {
if (task.isSuccessful()) {
// Sign in success, update UI with the signed-in user's information
Log.d(TAG, "createUserWithEmail:success");
FirebaseUser user = mAuth.getCurrentUser();
updateUI(user);
} else {
// If sign in fails, display a message to the user.
Log.w(TAG, "createUserWithEmail:failure", task.getException());
Toast.makeText(LoginActivity.this, "Authentication failed.",
Toast.LENGTH_SHORT).show();
updateUI(null);
}
}
});
}
private void signIn(String email, String password) {
mAuth.signInWithEmailAndPassword(email, password)
.addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
@Override
public void onComplete(@NonNull Task<AuthResult> task) {
if (task.isSuccessful()) {
// Sign in success, update UI with the signed-in user's information
Log.d(TAG, "signInWithEmail:success");
FirebaseUser user = mAuth.getCurrentUser();
updateUI(user);
} else {
// If sign in fails, display a message to the user.
Log.w(TAG, "signInWithEmail:failure", task.getException());
Toast.makeText(LoginActivity.this, "Authentication failed.",
Toast.LENGTH_SHORT).show();
updateUI(null);
}
}
});
}
@Override
public void onTransitionStarted(MotionLayout motionLayout, int startId, int endId) {
}
@Override
public void onTransitionChange(MotionLayout motionLayout, int startId, int endId, float progress) {
}
@Override
public void onTransitionCompleted(MotionLayout motionLayout, int currentId) {
if (currentId==R.id.end){
binding.btnLogin.setText(R.string.sign_up);
binding.textView3.setEnabled(false);
binding.textView2.setEnabled(true);
}else {
binding.btnLogin.setText(R.string.login);
binding.textView2.setEnabled(false);
binding.textView3.setEnabled(true);
}
}
@Override
public void onTransitionTrigger(MotionLayout motionLayout, int triggerId, boolean positive, float progress) {
}
}
Upvotes: 0
Reputation: 170
I just used this hack: click is handled programmatically, but it triggers the hidden view, on which <OnClick>
is registered in MotionScene
:
actualVisibleView.setOnClickListener {
doSomeLogic()
hiddenView.performClick()
}
And in MotionScene
:
<Transition
android:id="@+id/hackedTransitionThanksToGoogle"
motion:constraintSetEnd="@layout/expanded"
motion:constraintSetStart="@layout/closed"
motion:duration="300"
motion:motionInterpolator="linear">
<OnClick
motion:clickAction="transitionToEnd"
motion:targetId="@+id/hiddenView" />
</Transition>
Upvotes: 3
Reputation: 1359
You can also just handle the click programmatically from the beginning by removing
<OnClick app:target="@id/nextButton" />
altogether. Also it is easy to see whether or not your view is expanded by checking the progress of your transition. So you can programmatically handle it in your java/kotlin file with
yourButton.setOnClickListener {
if (yourMotionLayoutId.progress == 0.0)
yourMotionLayoutId.transitionToEnd
}
This way, it will check if the transition is in the state where it has not occurred (progress will be 0.0) and transition, otherwise, it will do nothing.
Upvotes: 10
Reputation: 372
I found a cleaner and more correct way to do it, you can do this .... OnClick directly from view ..
Note: It does not work with: <OnSwipe/>
only <OnClick/>
PD. I'm sorry, I'm from Mexico and I'm using the translator
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/play_pause_button_collapsed"
android:layout_width="30dp"
android:layout_height="50dp"
app:srcCompat="@drawable/ic_play_arrow_black_48dp"
android:layout_marginTop="25dp"
android:elevation="2dp"
android:alpha="0"
android:onClick="handleAction"
tools:ignore="ContentDescription" />
fun handleAction(view: View) {
//handle click
}
Upvotes: 2
Reputation: 181
<OnClick
motion:targetId="@+id/rateUsButton"
motion:clickAction="transitionToEnd"/>
Just ran into the same issue today. I was able to intercept the click by using setOnTouchListener
instead of setOnClickListener
in my code.
rateUsButton.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_UP) {
// handle the click
}
false
}
I know this solution isn't the best but I didn't find another option. Returning false means the touch was not handled here and thus will be handled by the motion layout.
Upvotes: 15