How to 2-way bind a Seekbar in custom view / component? - android

I'm trying to use 2-way databinding on a custom view that contains a SeekBar. The layout is rather simple, but I need to reuse it across the project, hence wrapping it into a custom view/component
<androidx.constraintlayout.widget.ConstraintLayout ... />
<TextView .../>
<TextView .../>
<SeekBar
android:id="#+id/ds_seekbar"
android:layout....
android:max="9"
android:min="0"
android:progress="0"
</androidx.constraintlayout.widget.ConstraintLayout>
The backing code looks like so (reduced)
CustomView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr), View.OnClickListener {
init {
LayoutInflater.from(context).inflate(R.layout.custom_view, this, true)
ds_description.setOnClickListener(this)
}
override fun onClick(view: View) {
//onClick implementation
}
}
I can do the binding in the ViewModel for the layout where this custom view is going to be used, with a BindingAdapter there with custom attribute (ex. app:seekbar), but the custom view would be used multiple times and I'd prefer to have the a lot of the logic that is required into the view and have a "lighter" handling in the ViewModel.
I read Android 2-Way DataBinding With Custom View and Custom Attr and a bunch of other articles which seem to be a little different but oon the same topic, however no matter how I wrote the getter and setters I always run into the kapt exception that it cannot find the getter/setter.
Either I'm not annotating properly the methods or they have wrong signatures.
Ideally I want to have something like:
CustomView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr), View.OnClickListener, SeekBar.OnProgressChangedListener {
... ds_seekbar.setOnProgressChangedListener(this)
And then in the main layout have the app:progress (or even better if someone can show how it's done android:progress) on the custom view for binding when passing my object.

Okay after more and more headscratching, here's what I've come with, that seems to work. Whether this is the proper way or how performant/reliable is - I'm not sure
#InverseBindingMethods(InverseBindingMethod(type = CustomView::class, attribute = "progress", event = "progressAttrChanged"))
CustomView #JvmOverloads constructor(...
private var progress = 0
private var mInverseBindingListener: InverseBindingListener? = null
cv_seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, i: Int, b: Boolean) {
progress = i + 1
if (mInverseBindingListener != null) {
mInverseBindingListener!!.onChange()
cv_indicator.text = progress.toString()
}
}...
})
fun getProgress(): Int {
return progress
}
fun setProgress(p: Int) {
if (progress != p) {
progress = p
}
}
fun setProgressAttrChanged(inverseBindingListener: InverseBindingListener?) {
if (inverseBindingListener != null) {
mInverseBindingListener = inverseBindingListener
}
}
Then the XML is
<com.xxx.CustomView
android:id="#+id/xxx"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:progress="#={viewModel.dataobject.value}"
....
/>

Related

How to write a test if a view is visible or not when it is set with data binding?

I have a custom view that is initially not visible. However this is determined with a binding adapter. My question as simple as it sounds, how do I write a test to check if the view is visible when the binding adapter method is called?
For example, this is my custom view:
class MyView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
fun setVisibile(visible: Boolean) {
this.visibility = if (visible) VISIBLE else GONE
}
}
And this is the binding adapter method :
#BindingAdapter(“visible)
#JvmStatic
fun setVisible(myView: MyView, someObject: SomeObject?) {
// Some checking on someObject
// ....
myView.setVisible(someObject.someCriteria())
}

Android/Kotlin, how to reduce duplicated part of this code

I'm a library author and have to intercept all touch events of child views, by overriding ViewGroup.onInterceptTouchEvent().
First I wrote the following code (simplified):
interface touchIntercepter {
// my library set this field to intercept touch event
var touchHandler: ((MotionEvent) -> Boolean)?
}
class LinearLayoutTouchIntercepter #JvmOverloads constructor (
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
)
: touchIntercepter
, LinearLayout(context, attrs, defStyleAttr, defStyleRes)
{
override var touchHandler: ((MotionEvent) -> Boolean)? = null
override fun onInterceptTouchEvent(event: MotionEvent) = touchHandler?.invoke(event) ?: false
}
Library users can use the LinearLayoutTouchInterceptor in their layout xml file instead of standard LinearLayout and then my library code can intercept touch event of the user layout's child views by touchIntercepter interface.
I think it is wonderful if there's something like ViewGroup.setOnInterceptTouchListener(), like View.setOnClickListener(), but I found that there isn't.
Now the problem is, I want to provide the same functionality for RelativeLayout, FrameLayout and other ViewGroup descendants.
For example,
class RelativeLayoutTouchIntercepter #JvmOverloads constructor (
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
)
: touchIntercepter
, RelativeLayout(context, attrs, defStyleAttr, defStyleRes)
{
override var touchHandler: ((MotionEvent) -> Boolean)? = null
override fun onInterceptTouchEvent(event: MotionEvent) = touchHandler?.invoke(event) ?: false
}
As you can see, all code is the same but the only difference is inheriting XXXXXLayout instead of LinearLayout. I don't want to copy and paste them but have no idea how to reduce the duplication.
It seems that Kotlin generics are not helping in this case while C++ template perfectly can help like this pseudo code :
template <typename T>
class TouchInterceptorTmpl : public T
{
void onInterceptTouchEvent() override;
};
using RelativeLayoutTouchInterceptor = TouchInterceptorTmpl<RelativeLayout>;
using FrameLayoutTouchInterceptor = TouchInterceptorTmpl<FrameLayout>;
No way to do like this in Kotlin?
You can reduce duplication a little bit by making a concrete implementation of your interface and using it as a delegate. Unfortunately, you can't avoid overriding onInterceptTouchEvent in each implementation due to how inheritance works, but you can make an extension function for your interface to shorten that code a bit.
Note, interface names in Kotlin are capitalized by convention.
Setup:
interface TouchInterceptor {
var touchInterceptionHandler: ((MotionEvent) -> Boolean)?
}
class TouchInterceptorImpl: TouchInterceptor {
override var touchInterceptionHandler: ((MotionEvent) -> Boolean)? = null
}
fun TouchInterceptor.intercept(event: MotionEvent): Boolean = touchInterceptionHandler?.invoke(event) ?: false
Usage:
class RelativeLayoutTouchIntercepter #JvmOverloads constructor (
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
)
: TouchInterceptor by TouchInterceptorImpl()
, RelativeLayout(context, attrs, defStyleAttr, defStyleRes)
{
override fun onInterceptTouchEvent(event: MotionEvent): Boolean = intercept(event)
}

How to use View Binding on custom views

View Binding got released as part of Android Jetpack
Docs: https://developer.android.com/topic/libraries/view-binding
My question is, how to use view binding with custom views. Google documentation has only show-cased Activity and fragment.
I tried this, but nothing was shown.
LayoutInflater inflater = LayoutInflater.from(getContext());
And then, I used this one, but again, no luck.
LayoutInflater inflater = (LayoutInflater)
getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
I guess maybe I don't target the correct layout inflater for my view but not sure.
Just inform the root, and whether you want to attach to it
init { // inflate binding and add as view
binding = ResultProfileBinding.inflate(LayoutInflater.from(context), this)
}
or
init { // inflate binding and add as view
binding = ResultProfileBinding.inflate(LayoutInflater.from(context), this, true)
}
which inflate method to use will depend on the root layout type in xml.
To use the view binding, you need to use the generated binding class not the LayoutInflater, for example, if the layout name is result_profile.xml then you need to use ResultProfileBinding as:
class CustomView #kotlin.jvm.JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private lateinit var binding: ResultProfileBinding
init { // inflate binding and add as view
binding = ResultProfileBinding.inflate(LayoutInflater.from(context))
addView(binding.root)
}
}
Auto generated class : result_profile.xml -> ResultProfileBinding(name of layout, appended with Binding )
Inflate the binding
ResultProfileBinding.inflate(LayoutInflater.from(context))
Use addView to add the view in the hierarchy as:
addView(binding.root)
Note: If you are extending from ConstraintLayout(is the parent class) then use constraint set
You can initialize the view binding property right away
private val binding = CustomViewBinding.inflate(LayoutInflater.from(context), this)
If you are trying to use View Binding with the root view, this is working for me:
class CustomView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
private lateinit var binding: CustomViewBinding
override fun onFinishInflate() {
super.onFinishInflate()
binding = CustomViewBinding.bind(this)
}
}
This is the simplest kotlin answer I can think of. It's a custom view that just wraps a single TextView and provides an update(s:String) function to update the text.
<!-- view_stub.xml -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<TextView android:id="#+id/myTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</layout>
// StubView.kt
class StubView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context,attrs,defStyleAttr) {
val binding = ViewStubBinding.inflate(context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater)
.also { addView(it.root) }
fun update(updatedText: String) {
binding.myTextView.text = updatedText
}
}
The two things I like about this answer are:
binding is a val instead of a var. I try to limit the number of vars as much as possible.
The addView is closely associated with the val binding using the also {} scope function instead of an init {} clause, making the instantiation of the View feel much more declarative.
One could argue that the addView() is really a side effect and should be in the init {} section so that it is separate from the declaration of the binding val. I would argue the opposite -- declaring a val then feeding it to a section of code that needs it does not feel like a side effect to me.
You can use DataBindingUtil
binding = DataBindingUtil.inflate(
LayoutInflater.from(context),
R.layout.your_layout_id,
this,
true
)

Why are my custom view RecyclerView items "invisible"

I need to onDraw the items of a RecyclerView. Using an approach "discovered" at this SO link, I have gotten - um - partway there.
Note that I ultimately want to onDraw "over" the custom view. Meaning call super to let the default drawing occur, then paint over unused areas of the (view's) canvas.
Before starting down this "custom view to allow onDraw" road, I had what you see on the left below:
Afterward, I had all "invisible" views (middle image above). I say "invisible" because they were still there to be clicked. To help me visualize things (and a bit of onDraw proof of concept) I overrode onDraw in the custom PuzzleView view, simply calling canvas.drawRect to cover the entire canvas in Green, and now see the right image above.
I am not sure what I'm doing wrong here.
Also, if it occurs to you that - well, why don't I simply onDraw the whole thing - that's not practical for a variety of reasons.
So, here's my PuzzleAdapter as it is now:
class PuzzleAdapter(private val puzzles: List<Puzzle>) : RecyclerView.Adapter<PuzzleAdapter.PuzzleHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PuzzleAdapter.PuzzleHolder {
//val v = LayoutInflater.from(parent.context).inflate(R.layout.item_puzzle, parent, false)
//not inflating, creating PuzzleView (which is inflating itself)
val v = PuzzleView(parent.context)
v.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
return PuzzleHolder(v)
}
override fun getItemCount() = puzzles.size
//unchanged between these two versions
override fun onBindViewHolder(h: PuzzleAdapter.PuzzleHolder, pos: Int) {
val p = puzzles[pos]
h.view.puzzleItem_text_ndx.text = "# " + p.descr()
h.view.puzzleItem_text_details.text = "Ndx: ${p.puzzleNdx}"
h.view.setOnClickListener {
Log.d("##", "PuzzleHolder.onClick (bunch#${p.parentBunch.bunchID}; puzzle#${p.puzzleNdx})")
val action = BunchFragmentDirections.navBunchToPuzzle(PuzzleParcel(p.parentBunch.bunchID, p.puzzleNdx))
it.findNavController().navigate(action)
}
}
inner class PuzzleHolder(v: View) : RecyclerView.ViewHolder(v) {
val view: PuzzleView
init {
view = v as PuzzleView
}
}
}
PuzzleAdapter before:
class PuzzleAdapter(private val puzzles: List<Puzzle>) : RecyclerView.Adapter<PuzzleAdapter.PuzzleHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PuzzleAdapter.PuzzleHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.item_puzzle, parent, false)
return PuzzleHolder(v)
}
override fun getItemCount() = puzzles.size
//unchanged between these two versions
override fun onBindViewHolder(h: PuzzleAdapter.PuzzleHolder, pos: Int) {
val p = puzzles[pos]
h.view.puzzleItem_text_ndx.text = "# " + p.descr()
h.view.puzzleItem_text_details.text = "Ndx: ${p.puzzleNdx}"
h.view.setOnClickListener {
Log.d("##", "PuzzleHolder.onClick (bunch#${p.parentBunch.bunchID}; puzzle#${p.puzzleNdx})")
val action = BunchFragmentDirections.navBunchToPuzzle(PuzzleParcel(p.parentBunch.bunchID, p.puzzleNdx))
it.findNavController().navigate(action)
}
}
inner class PuzzleHolder(v: View) : RecyclerView.ViewHolder(v) {
var view: View = v
}
}
PuzzleView (the custom view I am using with the Adapter):
class PuzzleView : RelativeLayout {
constructor (context: Context) : super(context) { init(context, null, 0) }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { init(context, attrs, 0) }
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) { init(context, attrs, defStyle) }
private fun init(context: Context, attrs: AttributeSet?, defStyle: Int) {
inflate(getContext(), R.layout.item_puzzle, this)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
//this.layout(l, t, r, b)
}
val rect = Rect(0, 0, 0, 0)
val paint = Paint()
override fun onDraw(canvas: Canvas) {
//super.onDraw(canvas)
Log.d("##", "PuzzleView.onDraw()")
rect.right = width - 10
rect.bottom = height - 10
val bkgrColor = ContextCompat.getColor(App.context, R.color.Green)
paint.style = Paint.Style.FILL
paint.color = bkgrColor
canvas.drawRect(rect, paint)
}
}
A few thoughts on the above class/code:
override fun onLayout is required
I have other custom views that work fine (not in relation to a RecyclerView) with an empty onLayout
I tried (it's commented out in the above code) this.layout(l, t, r, b) but get a stack overflow exception
My only real thoughts here are (1) that there's something I'm supposed to be doing in this onLayout method, but I can't think of what; or (2) there's something wrong with the way that I'm inflating item_puzzle, but - again - I can't think of what. (I tried a few things on this, to no avail). I cannot think of anything else!
And here's all the other code I think could possibly be relevant:
From the Fragment containing the RecyclerView (it's what is shown in the above three images):
bunch_recycler.layoutManager = GridLayoutManager(this.context, 3)
bunch_recycler.adapter = PuzzleAdapter(bunch.puzzles)
Finally, the XML for the item itself, item_puzzle:
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:cardBackgroundColor="#color/facadeLight"
app:cardElevation="0dp"
>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#drawable/shape_puzzle"
>
<TextView
android:id="#+id/puzzleItem_text_ndx"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:fontFamily="#font/showg"
android:maxLines="1"
android:text="17"
android:textColor="#color/facadeDark"
android:textSize="32sp"
/>
<TextView
android:id="#+id/puzzleItem_text_details"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#+id/puzzleItem_text_ndx"
android:layout_marginStart="8dp"
android:text="details"
android:textSize="12sp"
/>
</RelativeLayout>
</androidx.cardview.widget.CardView>
Also, if "custom view to allow onDraw" isn't the correct (or best) way of accomplishing my goal here, please let me know that as well.
Using:
Windows 10
Android Studio 3.4
Kotlin 1.3.31
Gradle 3.4.0
and the following:
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0-alpha05'
implementation 'androidx.core:core-ktx:1.2.0-alpha01'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0-beta01'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0-beta01'
implementation 'androidx.navigation:navigation-fragment-ktx:2.0.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.0.0'
Afterthought
I saw (in a 4 1/2-year-old youtube video overriding getView to assign subviews in a similar-seeming situation. There is no getView method to override in today's RecyclerView. The closest thing I see is getItemViewType(position: Int): Int which doesn't seem promising either (I read up on it a bit). Just thought I'd mention this in case it triggers a thought for you (where it didn't for me).

Kotlin Anko Custom View Parent scope

If we are building a custom View, for example, something like this:
class FrameLayoutNormal: FrameLayout{
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
textView{
lparams(...)
}
}
we can't define lparams, because the compiler doesn't know who the parent is. If we wrap the textView inside a FrameLayout it works, and you scan specify a layout parameter. But in a custom view, the parent is itself. So how can we make the children be aware of that so we can use the extension?
Is there any way to get it working, besides extending from: _FrameLayout ?`
An old question, but since it is common ...
Applying the answer from https://github.com/Kotlin/anko/issues/267
I think you might want something like this:
class FrameLayoutNormal: AnkoComponent<Context> {
override fun createView(ui: AnkoContext<Context>): View {
return with(ui) {
frameLayout {
textView("Hello") {
}.lparams()
}
}
}
}
inline fun ViewManager.frameLayoutNormal(theme: Int = 0) = frameLayoutNormal(theme) {}
inline fun ViewManager.frameLayoutNormal(theme: Int = 0, init: View.(frameLayoutNormal: FrameLayoutNormal) -> Unit): View {
val fln = FrameLayoutNormal()
return ankoView({ fln.createView(AnkoContext.create(it))}, theme, {init(fln)})
}
This allows the component to be used in the ANKO DSL. One downside to this approach is that the custom component is a View, not a ViewGroup, and thus can not have additional children added outside of its definition. It is challenging/laborious to make a custom component which is a ViewGroup that can be used in ANKO DSL (if I understand correctly).

Categories

Resources