How to create Binding Adapter for material.Slider view? - android

My goal is to 2-way databind material.Slider view to MutableLiveData from my viewmodel:
<com.google.android.material.slider.Slider
...
android:value="#={viewmodel.fps}"
...
/>
Of course, it's not working because there is no databinding adapter for Slider in androidx.databinding library
[databinding] Cannot find a getter for <com.google.android.material.slider.Slider android:value> that accepts parameter type <java.lang.Integer>. If a binding adapter provides the getter, check that the adapter is annotated correctly and that the parameter type matches.
But, they have one for SeekBar: /androidx/databinding/adapters/SeekBarBindingAdapter.java
As I understand, 2-way databinding should work only with "progress" attribute, and 1-way databinding requires two attributes: "onChanged" and "progress"
I made a try to adapt SeekBarBindingAdapter for Slider:
#InverseBindingMethods({
#InverseBindingMethod(type = Slider.class, attribute = "android:value"),
})
public class SliderBindingAdapter {
#BindingAdapter("android:value")
public static void setValue(Slider view, int value) {
if (value != view.getValue()) {
view.setValue(value);
}
}
#BindingAdapter(value = {"android:valueAttrChanged", "android:onValueChange"}, requireAll = false)
public static void setOnSliderChangeListener(Slider view, final Slider.OnChangeListener valChanged, final InverseBindingListener attrChanged) {
if (valChanged == null)
view.addOnChangeListener(null);
else
view.addOnChangeListener((slider, value, fromUser) -> {
if (valChanged != null)
valChanged.onValueChange(slider, value, fromUser);
});
if (attrChanged != null) {
attrChanged.onChange();
}
}
#Override
public void onValueChange(#NonNull Slider slider, float value, boolean fromUser) {
}
It's not building:
Could not find event android:valueAttrChanged on View type Slider
but why it looks for valueAttrChanged if I only use
android:value="#={viewmodel.fps}"
?
How do I find the right attribute to add to BindingAdapter, if I don't see valueAttrChanged in Slider class?

Let's look at SeekBarBindingAdapter's setOnSeekBarChangeListener() method. It adds four different attributes: {"android:onStartTrackingTouch", "android:onStopTrackingTouch", "android:onProgressChanged", "android:progressAttrChanged"} but only the last one is used by two-way databinding.
But why there are four attributes? If you look at SeekBar class, it has setOnSeekBarChangeListener() method which allows you to set and remove a listener. The problem is that SeekBar can only have one listener, and that listener provides different callbacks: onProgressChanged, onStartTrackingTouch and onStopTrackingTouch.
SeekBarBindingAdapter registers its own listener which means that no one can register another listener without removing the existing one. It's why SeekBarBindingAdapter provides onStartTrackingTouch, onStopTrackingTouch and onProgressChanged attributes, so you can listen to these events without registering your own OnSeekBarChangeListener.
Actually the Slider adapter can be much simpler than SeekBarBindingAdapter, because the Slider allows you to add and remove listeners using addOnChangeListener() and removeOnChangeListener(). So a two-way databinding adapter can register its own listener and anyone else can register other listeners without removing previous ones.
It allows us to define a pretty concise adapter. I created a kotlin example, hope you can translate it to java:
#InverseBindingAdapter(attribute = "android:value")
fun getSliderValue(slider: Slider) = slider.value
#BindingAdapter("android:valueAttrChanged")
fun setSliderListeners(slider: Slider, attrChange: InverseBindingListener) {
slider.addOnChangeListener { _, _, _ ->
attrChange.onChange()
}
}
And the layout:
...
<com.google.android.material.slider.Slider
...
android:value="#={model.count}" />
...
You can find the full sources here.

Update Android Java
Data binding
<variable
name="device"
type=".....Device" />
Trong file binding
#InverseBindingAdapter(attribute = "android:value")
public Float getSlider(Slider slider) {
return slider.getValue();
}
#BindingAdapter("app:valuesAttrChanged")
public void setSliderListeners(Slider slider, InverseBindingListener attrChange) {
slider.addOnChangeListener(new Slider.OnChangeListener() {
#Override
public void onValueChange(#NonNull Slider slider, float value, boolean fromUser) {
attrChange.onChange();
}
});
}
Trong file xml thêm dòng
android:value="#{device.data}"
data is value change

Related

Android - Update view outside a custom control (from inside custom control)

I have a custom control with buttons (image) that should update an activity (fragment) control (big 0 string).
May be it is too simple but I don't know how to do it because onClickListener buttons are inside the custom control class. Which is the best approach?
Listener inside custom control class is:
binding.counterSelectorViewPrevious.setOnClickListener(OnClickListener {
decreaseValue()}
fun decreaseValue() {
if (mSelectedIndex > 0) {
val newSelectedIndex = mSelectedIndex - 1
setSelectedIndex(newSelectedIndex)
}
}
Activity control is just a TextView outside of custom control class.
<TextView
android:id="#+id/workout_length"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="0"
android:textAlignment="textEnd"
android:textAppearance="#style/TextAppearance.AppCompat.Display2"
tools:text="99:99:99" />
Thanks in advance.
You need to accept a listener in your custom view what get's some callback from your activity/fragment
something like this
private OnValueChangeListener onValueChangeListener;
public void addOnValueChangeListener(OnValueChangeListener onValueChangeListener)
{
this.onValueChangeListener = onValueChangeListener;
}
public void decreaseValue() {
onValueChangeListener.onValueChange(newValue);
}
public interface OnValueChangeListener {
void onValueChange(int newValue);
}
then you need to add invoke this somewhere in your activity/fragment (like in onCreate/onCreateView)
void setupListener() {
addOnValueChangeListener(newValue -> {
findViewById(R.id.workout_length).setText("" + newValue);
});
}

Text Watcher Binding Adapter with ViewModel

I'm still new to all this MVVM and Android Architecture Components. I have a few screens (Login and Register) that have inputs for email, password, name etc and a 'continue' button that's only enabled when the required fields are filled out correctly. There's also text views for messages such as "password must be..." and "not a valid email...". I'm trying to use a binding adapter and viewmodel to pull some of this validation logic out of my MainActivity and away from my business logic, but I'm struggling. I'm not even sure if this is the best way to do it. My thought with the ViewModel is that it would hold it's state on rotation/activity changes.
RegisterActivity is an Activity that simply holds a ViewPager with 3 fragments (email/password, first/last name, validation code).
Binding Adapter
#BindingAdapter("app:onTextChanged")
public static void onTextChanged(TextInputEditText view, TextViewBindingAdapter.OnTextChanged listener) {
}
Layout
<TextView
android:id="#+id/tv_error_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="#string/register_email_requirement"
android:textColor="#color/moenPrimaryError"
android:textSize="18sp"
android:visibility="#{viewModel.emailValidationVisible ? View.VISIBLE: View.GONE}"
app:layout_constraintBottom_toTopOf="#id/input_layout_password"
app:layout_constraintEnd_toEndOf="#+id/input_layout_email_address"
app:layout_constraintStart_toStartOf="#+id/input_layout_email_address"
app:layout_constraintTop_toBottomOf="#+id/input_layout_email_address"
app:layout_goneMarginTop="16dp" />
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/input_email_address"
style="#style/MoenTextInputEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textEmailAddress"
app:onTextChanged="#{viewModel.onTextChanged}"
app:onFocusChange="#{inputLayoutEmailAddress}">
</com.google.android.material.textfield.TextInputEditText>
ViewModel
public class RegisterFragmentViewModel extends BaseObservable {
private boolean emailValidationVisible = false;
#Bindable
public boolean getEmailValidationVisible() {
return this.emailValidationVisible;
}
public void toggle() {
this.emailValidationVisible = !this.emailValidationVisible;
notifyPropertyChanged(BR.viewModel);
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
Log.w("tag", "onTextChanged " + s);
this.toggle();
}
}
This is just a test I have. My thought was that I could bind the TextView visibility to a boolean that I can toggle/manipulate with a onTextChanged listener, but I don't know how to wire up the Binding Adapter. Am I on the right path? Is there a better/easier way to do this?
Am I on the right path? Is there a better/easier way to do this?
it is a way of doing it. I would remove the ternary operator from this line
android:visibility="#{viewModel.emailValidationVisible ? View.VISIBLE: View.GONE}"
and create a simple function in your VM that returns it. EG:
#Bindable
public int getVisibility() {
return emailValidationVisible ? View.VISIBLE: View.GONE
}
and in toggle(), you will have something like
public void toggle() {
this.emailValidationVisible = !this.emailValidationVisible;
notifyPropertyChanged(BR.visibility);
}
which will call the getter for you. Your xml will have to be changed like follow
android:visibility="#{viewModel.getVisibility()}"
Alternately you could create a BindingAdapter that takes a Boolean and change the visibility accordingly to it

Use group in ConstraintLayout to listen for click events on multiple views

Basically I'd like to attach a single OnClickListener to multiple views inside a ConstraintLayout.
Before migrating to the ConstraintLayout the views where inside one layout onto which I could add a listener. Now they are on the same layer with other views right under the ConstraintLayout.
I tried adding the views to a android.support.constraint.Group and added a OnClickListener to it programmatically.
group.setOnClickListener {
Log.d("OnClick", "groupClickListener triggered")
}
However this does not seem to work as of the ConstraintLayout version 1.1.0-beta2
Have I done something wrong, is there a way to achieve this behaviour or do I need to attach the listener to each of the single views?
The Group in ConstraintLayout is just a loose association of views AFAIK. It is not a ViewGroup, so you will not be able to use a single click listener like you did when the views were in a ViewGroup.
As an alternative, you can get a list of ids that are members of your Group in your code and explicitly set the click listener. (I have not found official documentation on this feature, but I believe that it is just lagging the code release.) See documentation on getReferencedIds here.
Java:
Group group = findViewById(R.id.group);
int refIds[] = group.getReferencedIds();
for (int id : refIds) {
findViewById(id).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
// your code here.
}
});
}
In Kotlin you can build an extension function for that.
Kotlin:
fun Group.setAllOnClickListener(listener: View.OnClickListener?) {
referencedIds.forEach { id ->
rootView.findViewById<View>(id).setOnClickListener(listener)
}
}
Then call the function on the group:
group.setAllOnClickListener(View.OnClickListener {
// code to perform on click event
})
Update
The referenced ids are not immediately available in 2.0.0-beta2 although they are in 2.0.0-beta1 and before. "Post" the code above to grab the reference ids after layout. Something like this will work.
class MainActivity : AppCompatActivity() {
fun Group.setAllOnClickListener(listener: View.OnClickListener?) {
referencedIds.forEach { id ->
rootView.findViewById<View>(id).setOnClickListener(listener)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Referenced ids are not available here but become available post-layout.
layout.post {
group.setAllOnClickListener(object : View.OnClickListener {
override fun onClick(v: View) {
val text = (v as Button).text
Toast.makeText(this#MainActivity, text, Toast.LENGTH_SHORT).show()
}
})
}
}
}
This should work for releases prior to 2.0.0-beta2, so you can just do this and not have to do any version checks.
The better way to listen to click events from multiple views is to add a transparent view as a container on top of all required views. This view has to be at the end (i.e on top) of all the views you need to perform a click on.
Sample container view :
<View
android:id="#+id/view_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="#+id/view_bottom"
app:layout_constraintEnd_toEndOf="#+id/end_view_guideline"
app:layout_constraintStart_toStartOf="#+id/start_view_guideline"
app:layout_constraintTop_toTopOf="parent"/>
Above sample contains all four constraint boundaries within that, we can add views that to listen together and as it is a view, we can do whatever we want, such as ripple effect.
To complement the accepted answer for Kotlin users create an extension function and accept a lambda to feel more like the API group.addOnClickListener { }.
Create the extension function:
fun Group.addOnClickListener(listener: (view: View) -> Unit) {
referencedIds.forEach { id ->
rootView.findViewById<View>(id).setOnClickListener(listener)
}
}
usage:
group.addOnClickListener { v ->
Log.d("GroupExt", v)
}
The extension method is great but you can make it even better by changing it to
fun Group.setAllOnClickListener(listener: (View) -> Unit) {
referencedIds.forEach { id ->
rootView.findViewById<View>(id).setOnClickListener(listener)
}
}
So the calling would be like this
group.setAllOnClickListener {
// code to perform on click event
}
Now the need for explicitly defining View.OnClickListener is now gone.
You can also define your own interface for GroupOnClickLitener like this
interface GroupOnClickListener {
fun onClick(group: Group)
}
and then define an extension method like this
fun Group.setAllOnClickListener(listener: GroupOnClickListener) {
referencedIds.forEach { id ->
rootView.findViewById<View>(id).setOnClickListener { listener.onClick(this)}
}
}
and use it like this
groupOne.setAllOnClickListener(this)
groupTwo.setAllOnClickListener(this)
groupThree.setAllOnClickListener(this)
override fun onClick(group: Group) {
when(group.id){
R.id.group1 -> //code for group1
R.id.group2 -> //code for group2
R.id.group3 -> //code for group3
else -> throw IllegalArgumentException("wrong group id")
}
}
The second approach has a better performance if the number of views is large since you only use one object as a listener for all the views!
While I like the general approach in Vitthalk's answer I think it has one major drawback and two minor ones.
It does not account for dynamic position changes of the single views
It may register clicks for views that are not part of the group
It is not a generic solution to this rather common problem
While I'm not sure about a solution to the second point, there clearly are quite easy ones to the first and third.
1. Accounting position changes of element in the group
This is actually rather simple. One can use the toolset of the constraint layout to adjust the edges of the transparent view.
We simply use Barriers to receive the leftmost, rightmost etc. positions of any View in the group.
Then we can adjust the transparent view to the barriers instead of concrete views.
3. Generic solution
Using Kotlin we can extend the Group-Class to include a method that adds a ClickListener onto a View as described above.
This method simply adds the Barriers to the layout paying attention to every child of the group, the transparent view that is aligned to the barriers and registers the ClickListener to the latter one.
This way we simply need to call the method on the Group and do not need to add the views to the layout manually everytime we need this behaviour.
in Constraintlayout 2.0.0,you can use Layer to resolve multiple views click event,and also support scale animation
For the Java people out there like me:
public class MyConstraintLayoutGroup extends Group {
public MyConstraintLayoutGroup(Context context) {
super(context);
}
public MyConstraintLayoutGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyConstraintLayoutGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setOnClickListener(OnClickListener listener) {
for (int id : getReferencedIds()) {
getRootView().findViewById(id).setOnClickListener(listener);
}
}
}
This is not propagating click states to all other children however.
fun ConstraintLayout.setAllOnClickListener(listener: (View) -> Unit) {
children.forEach { view ->
rootView.findViewById<View>(view.id).setOnClickListener { listener.invoke(this) }
}
}
And then
.setAllOnClickListener {
do something
}

Can I have BindingAdapters with overlapping value sets?

Let's say I have this view model with methods:
public int getValueA() {
return a;
}
public int getValueB() {
return b;
}
#BindingAdapter("valueA")
public void setupSomething(View view, int valueA) {
// do something with a
}
#BindingAdapter({"valueA", "valueB"})
public void setupSomethingElse(View view, int valueA, int valueB) {
// do something with a and b
}
and I bind this to a view:
<View
android:layout_width="match_parent"
android:layout_height="wrap_content"
bind:valueA="#{viewmodel.valueA}"
bind:valueB="#{viewmodel.valueB}"/>
How can I call both BindingAdapter methods? Right now data binding is just calling the later. I guess I could just call setSomething from within setSomethingElse but this smells a little fishy to me (and partially defeats the purpose of databinding).
It's like you're suggesting yourself: you need to call setupSomething() from setupSomethingElse. It's just fine doing so and how databindings work. Only the best fitting #BindingAdapter will be used for your attributes.
Alternatively you can use the requireAll() field of #BindingAdapter. But this is only feasible if you can handle the Java default value (in your case 0) for your values.
Whether every attribute must be assigned a binding expression or if some can be absent. When this is false, the BindingAdapter will be called when at least one associated attribute has a binding expression. The attributes for which there was no binding expression (even a normal XML value) will cause the associated parameter receive the Java default value. Care must be taken to ensure that a default value is not confused with a valid XML value.
#BindingAdapter({"valueA", "valueB"}, requireAll = false)
public void setupSomethingElse(View view, int valueA, int valueB) {
if (valueA != 0) {
// do something with a
if (valueB != 0) {
// do something with a and b
}
}
}
So you don't need setupSomething() anymore. But personally I like the first approach better.

Xamarin.Forms swipe gesture recognizer

Xamarin.Forms is very new and very exciting, but for now I see that it has limited documentation and a few samples. I'm trying to make an app with an interface similar to the "MasterDetailPage" one, but also having a right Flyout view, not only the left one.
I've seen that this is not possible using the current API, and so my approach was this:
Create a shared GestureRecognizer interface.
In Android app and iOS in bind this interface to the UIGestureRecognizer on iOS or the OnTouch method on the android.
For iOS this is working but for Android the touch listener over the activity doesn't seem to work.
Is my approach good? Maybe there is another good method to capture touch events directly from the shared code? Or do you have any ideas why the public override bool OnTouchEvent doesn't work in an AndroidActivity?
For Xamarin.Forms swipe gesture recognizer add SwipeGestureRecognizer
<BoxView Color="Teal" ...>
<BoxView.GestureRecognizers>
<SwipeGestureRecognizer Direction="Left" Swiped="OnSwiped"/>
</BoxView.GestureRecognizers>
</BoxView>
Here is the equivalent C# code:
var boxView = new BoxView { Color = Color.Teal, ... };
var leftSwipeGesture = new SwipeGestureRecognizer { Direction = SwipeDirection.Left };
leftSwipeGesture.Swiped += OnSwiped;
boxView.GestureRecognizers.Add(leftSwipeGesture);
For more check here : https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/gestures/swipe
The MasterDetailPage, and other shared elements, are just containers for view renderers to pick up. You best option would be to create a custom LRMasterDetailPage (left-right..) and give it properties for both the DetailLeft and DetailRight. Then you implement a custom ViewRenderer per platform for this custom element.
The element:
public class LRMasterDetailPage {
public View LeftDetail;
public View RightDetail;
public View Master;
}
The renderer:
[assembly:ExportRenderer (typeof(LRMasterDetailPage), typeof(LRMDPRenderer))]
namespace App.iOS.Renderers
{
public class LRMDPRenderer : ViewRenderer<LRMasterDetailPage,UIView>
{
LRMasterDetailPage element;
protected override void OnElementChanged (ElementChangedEventArgs<LRMasterDetailPage> e)
{
base.OnElementChanged (e);
element = e.NewElement;
// Do someting else, init for example
}
protected override void OnElementPropertyChanged (object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "Renderer")
return;
base.OnElementPropertyChanged (sender, e);
if (e.PropertyName == "LeftDetail")
updateLeft();
if (e.PropertyName == "RightDetail")
updateRight();
}
private void updateLeft(){
// Insert view of DetailLeft element into subview
// Add button to open Detail to parent navbar, if not yet there
// Add gesture recognizer for left swipe
}
private void updateRight(){
// same as for left, but flipped
}
}
}
I advise you to use the CarouselView approach, f.e. you can use already existing solutions with carousel view which are supports swipe gestures. So in result your view will be wrapped into this carousel view control

Categories

Resources