I want to create a Custom or Compound View (a combination of several standard components) in Android to which I can bind variables.
Here is a simple shorted example:
The new control (view_custom.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="match_parent"
android:orientation="vertical">
<EditVext android:id="#+id/edittext_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:textStyle="bold" android:layout_gravity="center" />
<EditVext
...
</LinearLayout>
I want use the control in my fragment with two-way-binding
<com.test.controls.CustomView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="#={viewmodel.title}"
/>
I´ve tried to create attr like this
<declare-styleable name="Custom">
<attr name="title" format="string" />
</declare-styleable>
and
class CustomView(context: Context, attributeSet: AttributeSet?) :
LinearLayout(context, attributeSet) {
init {
val a = context.obtainStyledAttributes(attributeSet, R.styleable.CustomView, 0, 0)
val titleText = a.getString(R.styleable.CustomView_titl
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
inflater.inflate(R.layout.view_custom, this, true)
edittext_title.text = titleText
}
fun setTitle(title String) {
edittext_title.text = title
}
fun getTitleText(): String {
return edittext_title.text.toString()
}
}
The one way binding works but the two-way doesn´t.
Here is my solution.
I´ve created an interface to notify that the "title" var changed
interface TwoWayBindingListener {
fun hasChanged()
}
Then I extended my CustomView class
private var listener: TwoWayBindingListener? = null
fun addListener(twoWayBindingListener: TwoWayBindingListener) {
listener = twoWayBindingListener
}
And call the hasChanged method whenever I changed the title inside mit control
listener?.hasChanged()
And I add these BindingAdapters
#BindingAdapter("title")
fun set#String(customView: CustomView, t: String?) {
if (t == null) {
return
}
customView.setTitle()
}
#InverseBindingAdapter(attribute = "title", event = "titleAttrChanged")
fun getDateString(customView: CustomView): String {
return customView.getTitle()
}
#BindingAdapter("titleAttrChanged")
fun setListener(customView: CustomView, listener: InverseBindingListener?) {
if (listener != null) {
customView.addListener(
object : TwoWayBindingListener {
override fun hasChanged() {
listener.onChange()
}
})
}
}
Related
What I'm trying to accomplish here, is to create an custom view of ConstraintLayout wrapping a InputTextLayout and also edittext,along with a textView.
However the setting functions aren't working when setting in fragment(DataBinding). And also with the edittext, I was hoping to try two-way binding for LiveData and Observer.
Please try to approach with Kotlin
Attrs.xml
<resources>
<declare-styleable name="ErrorCasesTextInputLayout">
<attr name="isPass" format="boolean" />
<attr name="errorCase" format="enum">
<enum name="empty" value="0"/>
<enum name="format" value="1"/>
<enum name="identical" value="2"/>
</attr>
<attr name="text" format="string" value=""/>
<attr name="hint" format="string" value=""/>
</declare-styleable>
Custom View Layout
<androidx.constraintlayout.widget.ConstraintLayout
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="match_parent">
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/custom_text_input_layout"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginTop="20dp"
android:layout_marginHorizontal="20dp"
android:focusable="false"
android:focusableInTouchMode="true"
android:paddingBottom="2dp"
android:background="#drawable/bg_edittext"
app:hintEnabled="false"
app:boxBackgroundMode="none"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/custom_edit_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="20dp"
android:background="#null"
android:imeOptions="actionGo"
android:ellipsize="middle"
android:singleLine="true"
android:inputType="text"
android:textSize="15sp">
</com.google.android.material.textfield.TextInputEditText>
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="#+id/custom_error_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This field is required"
android:textSize="12sp"
android:layout_marginBottom="5dp"
android:textColor="#color/errorRed"
android:visibility="gone"
app:layout_constraintStart_toStartOf="#id/custom_text_input_layout"
app:layout_constraintTop_toBottomOf="#id/custom_text_input_layout"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Custom View Class
class ErrorCasesTextInputLayout(context: Context, attrs: AttributeSet) :
ConstraintLayout(context, attrs) {
private var _errorCase: Int
private var _isPass: Boolean
private var _hint: String?
private var _text: String?
init {
LayoutInflater.from(context)
.inflate(R.layout.custom_error_case_text_input_layout, this, true)
attrs.let {
val attributes =
context.obtainStyledAttributes(it, R.styleable.ErrorCasesTextInputLayout)
attributes.apply {
try {
_isPass = this.getBoolean(R.styleable.ErrorCasesTextInputLayout_isPass, true)
_errorCase = this.getInteger(R.styleable.ErrorCasesTextInputLayout_errorCase, 0)
_hint = this.getString(R.styleable.ErrorCasesTextInputLayout_hint)
_text = this.getString(R.styleable.ErrorCasesTextInputLayout_text)
mSetErrorCase()
mSetPass()
mSetHint()
} finally {
recycle()
}
}
}
}
fun setErrorCase(caseType: Int) {
_isPass = false
_errorCase = caseType
invalidate()
requestLayout()
}
private fun mSetHint() {
val editText = findViewById<TextInputEditText>(R.id.custom_edit_text)
if (_hint != null ) {
editText.hint = _hint
}
}
private fun mSetPass() {
val layout = findViewById<View>(R.id.custom_text_input_layout)
if (_isPass) {
layout.setBackgroundResource(R.drawable.bg_edittext)
} else {
layout.setBackgroundResource(R.drawable.bg_edittext_error)
}
}
private fun mSetErrorCase() {
val errorText = findViewById<TextView>(R.id.custom_error_message)
val layout = findViewById<View>(R.id.custom_text_input_layout)
when (_errorCase) {
0 -> {
errorText.text = EdittextErrorCase.EMPTY.errorMessage
errorText.visibility = View.VISIBLE
layout.setBackgroundResource(R.drawable.bg_edittext_error)
}
1 -> {
errorText.text = EdittextErrorCase.FORMAT.errorMessage
errorText.visibility = View.VISIBLE
layout.setBackgroundResource(R.drawable.bg_edittext_error)
}
2 -> {
errorText.text = EdittextErrorCase.UNIDENTICAL.errorMessage
errorText.visibility = View.VISIBLE
layout.setBackgroundResource(R.drawable.bg_edittext_error)
}
}
}
fun setPass(pass: Boolean) {
_isPass = pass
invalidate()
requestLayout()
}
fun setText(text: String) {
_text = text
invalidate()
requestLayout()
}
fun setHint(hint: String) {
_hint = hint
invalidate()
requestLayout()
}
fun getCurrentErrorCase(): Int {
return _errorCase
}
#InverseBindingMethods(InverseBindingMethod(
type = ErrorCasesTextInputLayout::class,
attribute = "bind:text",
event = "bind:textAttrChanged",
method = "bind:getText")
)
class CustomEditTextBinder {
companion object {
#BindingAdapter("textAttrChanged")
#JvmStatic
fun setListener(view: ErrorCasesTextInputLayout, listener: InverseBindingListener) {
val input: TextInputEditText = view.findViewById(R.id.custom_edit_text)
input.addTextChangedListener(object : TextWatcher{
override fun afterTextChanged(p0: Editable?) {
listener.onChange()
}
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
})
}
#BindingAdapter("text")
#JvmStatic
fun setTextValue(view: ErrorCasesTextInputLayout, value: String?) {
if (value != view._text) view.setText(value.toString())
}
#InverseBindingAdapter(attribute = "text", event = "textAttrChanged")
#JvmStatic
fun getTextValue(view: ErrorCasesTextInputLayout): String? = view._text
}
}
}
Working Fragment
class ChangeNumberFragment : Fragment() {
lateinit var binding: FragmentChangeNumberBinding
private val viewModel by viewModels<ChangeNumberViewModel> { getVmFactory() }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreate(savedInstanceState)
binding = FragmentChangeNumberBinding.inflate(inflater, container, false)
binding.viewModel = viewModel
binding.lifecycleOwner = this
binding.editTextNumber.setHint("Enter New Number")
binding.editTextNumber.setPass(true)
viewModel.newNumber.observe(viewLifecycleOwner, Observer {
if (it.isNullOrEmpty()) {
binding.editTextNumber.setErrorCase(1)
} else {
Logger.i(it)
binding.editTextNumber.setPass(true)
}
})
return binding.root
}
}
Two-Way Binding with liveData
app:text="#={viewModel.newNumber}"
After a day of researching and try error, I manage to success on the two-way binding.
It was an obvious error that I didn't assign the editText view to the getText method, and with an incorrect form of null handling on setText.
class CustomEditTextBinder {
companion object {
#JvmStatic
#BindingAdapter(value = ["textAttrChanged"])
fun setListener(view: ErrorCasesTextInputLayout, listener: InverseBindingListener?) {
if (listener != null) {
view.custom_edit_text.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
}
override fun afterTextChanged(editable: Editable) {
listener.onChange()
}
})
}
}
#JvmStatic
#InverseBindingAdapter(attribute = "text")
fun getText(view: ErrorCasesTextInputLayout): String {
return view.custom_edit_text.text.toString()
}
#JvmStatic
#BindingAdapter("text")
fun setText(view: ErrorCasesTextInputLayout, text: String?) {
text?.let {
if (it != view.custom_edit_text.text.toString()) {
view.custom_edit_text.setText(it)
}
}
}
}
}
Hope this helps for those who are also facing the same problem.
I have the following RecycleView adapter:
class PageViewerAdapter(val context: Context, private val pages: List<Page>) :
RecyclerView.Adapter<PageViewerAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(context);
val view = inflater.inflate(R.layout.page_viewer_card, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.testView.text = position.toString()
holder.panelView.isEnabled = false
}
override fun getItemCount(): Int {
return pages.size
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val testView: TextView = itemView.test_text
val panelView: PanelView = itemView.page_preview
}
}
The problem is that itemView.page_preview, which is referencing my custom view in the XML, is null. Both in ViewHolder initialization and in the onBindViewHolder.
This is the RecyclerView card/item XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:padding="10dp">
<TextView
android:id="#+id/test_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<my.package.PanelView
android:id="#+id/page_preview"
android:layout_width="0dp"
android:layout_height="match_parent"
app:layout_constraintDimensionRatio="W, 1:1.4142"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
If however, the page_preview references are removed from ViewHolder and onBindViewHolder, the views will load fine within a few seconds.
Update
This is the custom view class:
class PanelView:
(context: Context?, attrs: AttributeSet?) : SurfaceView(context), SurfaceHolder.Callback {
private var canvasThread: CanvasThread
init {
this.holder.addCallback(this)
canvasThread = CanvasThread(this.holder, this)
this.isFocusable = true
}
override fun surfaceCreated(holder: SurfaceHolder) {
resume()
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
pause()
}
fun resume() {
if (!canvasThread.isAlive) {
canvasThread = CanvasThread(this.holder, this)
canvasThread.run = true
canvasThread.start()
}
}
fun pause() {
if (canvasThread.isAlive) {
var retry = true
canvasThread.run = false
while (retry) {
try {
canvasThread.join()
retry = false
} catch (e: InterruptedException) {
}
}
}
}
}
You are probably using kotlinx.android.synthetic import statements to access your specific within the parent view. This does not work for recycler views, instead you should do val panelView = itemView.findViewById(R.id.page_preview)
Kotlinx synthetic doesn't work by default in RecyclerView.ViewHolder class, you need to have it extend kotlinx.android.extensions.LayoutContainer:
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), LayoutContainer {
val testView: TextView = itemView.test_text //now this will work
val panelView: PanelView = itemView.page_preview
}
Anyway, you should really be careful with syntetic imports as they can be a headache to debug, especially in fragments. You might as well just use the fancy new findViewById<View>
Edit 1:
Since you've posted your custom view class, at first glance it seems that the super constructor invocation isn't right. You could use the following for all custom view kotlin classes:
class CustomView #JvmOverloads constructor(
ctx: Context,
attrs: AttributeSet? = null,
defStyleAttrs: Int = 0
) : View(ctx, attrs, defStyleAttrs)
I am trying to implement some custom bindingadapters so that I can databind my viewmodel value to the switch.ischecked value in my custom view. I want the switch state to change if enabled in my viewmodel changes and visa-versa. I have looked at numerous articles on how to accomplish this, yet it still does nothing. I can see that my setSwitchChecked method is being used by the databinding implementation, but it doesn't seem to actually set anything. the other 2 adapters remain unused. Any help as so what I am missing or doing wrong is appreciated.
ViewModel
open class SettingsViewModel #Inject constructor(): ViewModel() {
var enabled: MutableLiveData<Boolean> = MutableLiveData()
}
Fragment
class SettingsFragment #Inject constructor(): Fragment() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var viewDataBinding: FragmentSettingsBinding
private lateinit var viewModel: SettingsViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view: View = inflater.inflate(R.layout.fragment_settings, container, false)
viewModel = ViewModelProviders.of(activity!!, viewModelFactory).get(SettingsViewModel::class.java)
viewDataBinding = FragmentSettingsBinding.inflate(inflater, container, false).apply {
viewmodel = viewModel
}
return view
}
}
CustomView xml binding
<com.stinson.sleepcycles.views.SwitchRow
android:id="#+id/switch_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:switchLabel="#string/enabled"
app:switchChecked="#{viewmodel.enabled}"/>
Custom View Class
class SwitchRow constructor(context: Context, attrs: AttributeSet, defStyle: Int = 0) :
RelativeLayout(context, attrs, defStyle), View.OnClickListener {
constructor(context: Context, attrs: AttributeSet): this(context, attrs, 0)
init {
val view = inflate(context, R.layout.view_switch_row, this)
val a = context.theme.obtainStyledAttributes(attrs, R.styleable.SwitchRow, defStyle, 0)
try {
view.text_label.text = a.getString(R.styleable.SwitchRow_switchLabel)
view.switch_toggle.isChecked = a.getBoolean(R.styleable.SwitchRow_switchChecked, false)
} finally {
a.recycle()
}
view.setOnClickListener {
view.switch_toggle.callOnClick()
}
view.switch_toggle.setOnClickListener {
toggleSwitch(view)
}
}
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_UP) {
this.callOnClick()
}
return super.dispatchTouchEvent(event)
}
override fun onClick(view: View?) {
if (view != null) view.callOnClick()
}
private fun toggleSwitch(view: View) {
view.switch_toggle.isChecked = !view.switch_toggle.isChecked
}
}
Custom View XML
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:padding="16dp">
<TextView
android:id="#+id/text_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="#dimen/text_size_medium"
tools:text="Label" />
<androidx.appcompat.widget.SwitchCompat
android:id="#+id/switch_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"/>
</RelativeLayout>
Binding Adapters
#BindingAdapter("switchCheckedAttrChanged")
fun setListener(switchRow: SwitchRow, listener: InverseBindingListener) {
switchRow.switch_toggle.setOnCheckedChangeListener { _, _ ->
listener.onChange()
}
}
#BindingAdapter("switchChecked")
fun setSwitchChecked(switchRow: SwitchRow, value: Boolean) {
if (value != switchRow.switch_toggle.isChecked) {
switchRow.switch_toggle.isChecked = value
}
}
#InverseBindingAdapter(attribute = "switchChecked")
fun getSwitchChecked(switchRow: SwitchRow): Boolean {
return switchRow.switch_toggle.isChecked
}
Your InverseBindingAdapter should have the following annotation:
#InverseBindingAdapter(attribute = "switchChecked", event = "switchCheckedAttrChanged")
I am a newbie with android databinding, but the ImageView doesn't bind in the RecyclerView. I have read several blogs but no luck. What am I missing?
Below are some of the blog posts I have read:
link 1
link2
Below is how I have styled my xml layout.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="movie"
type="com.movieapp.huxymovies.model.Result" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:background="#color/bg"
android:orientation="vertical">
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="0dp"
android:layout_marginTop="8dp"
android:background="#color/bg"
android:orientation="horizontal">
<ImageView
android:id="#+id/img"
android:layout_width="70dp"
android:layout_height="100dp"
android:layout_marginLeft="8dp"
app:movieImage="#{movie.MPosterPath}" />
</LinearLayout>
</LinearLayout>
</android.support.v7.widget.CardView>
</LinearLayout>
</layout>
Then this is the modal class which contains all the attributes:
#Entity(tableName = "Results")
class Result {
companion object {
#JvmStatic
#BindingAdapter("movieImage")
fun LoadImage(view: View, mPosterPath: String?) {
val imageView = view as ImageView
Glide.with(view.context)
.load(Utils.IMAGE_BASE_URL + mPosterPath)
.into(imageView)
}
#BindingAdapter("rating")
fun setRating(ratingBar: RatingBar, rating: Float) {
if (rating != null) {
ratingBar.rating = rating
}
}
}
constructor(mId: Long?, mOverview: String?, mPosterPath: String?, mTitle: String?, mVoteAverage: Double?) {
this.mId = mId
this.mOverview = mOverview
this.mPosterPath = mPosterPath
this.mTitle = mTitle
this.mVoteAverage = mVoteAverage
}
constructor()
#PrimaryKey
#SerializedName("id")
var mId: Long? = null
#SerializedName("overview")
var mOverview: String? = null
#SerializedName("poster_path")
var mPosterPath: String? = null
#SerializedName("title")
var mTitle: String? = null
#SerializedName("vote_average")
var mVoteAverage: Double? = null
}
Then finally, in my adapter class, I tried to bind the item layout.
class ResultAdapter(private val context: Context) : PagedListAdapter<Result, ResultAdapter.ResultViewHolder>(DIFF_CALLBACK) {
public lateinit var mBinding: ItemActivitymainBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ResultViewHolder {
mBinding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.item_activitymain, parent, false)
return ResultViewHolder(mBinding)
}
override fun onBindViewHolder(holder: ResultViewHolder, position: Int) {
val result = getItem(position)
if (result != null) {
holder.itemActivitymainBinding.titleTxt.text = result.mTitle
}
}
class ResultViewHolder(itemView: ItemActivitymainBinding) : RecyclerView.ViewHolder(itemView.root) {
var itemActivitymainBinding: ItemActivitymainBinding
var root: View
init {
root = itemView.root
itemActivitymainBinding = itemView
}
}
companion object {
const val MOVIE_ID = "MOVIE_ID"
const val MOVIE_NAME = "MOVIE_NAME"
const val MOVIE_OVERVIEW = "MOVIE_OVERVIEW"
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Result>() {
override fun areItemsTheSame(oldItem: Result, newItem: Result): Boolean {
return oldItem.mId === newItem.mId
}
override fun areContentsTheSame(oldItem: Result, newItem: Result): Boolean {
return oldItem == newItem
}
}
}
}
Now I am still wondering why the image doesn't display because I have read some of the blog posts about this and I followed all their procedures.
First, you binding is missing its lifecycle owner (i.e., the activity or fragment in which you use the adapter). You should pass it to your adapter and then set it:
class ResultAdapter(private val lifecycleOwner: LifecycleOwner)
: PagedListAdapter<Result, ResultAdapter.ResultViewHolder>(DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ResultViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = DataBindingUtil.inflate<ItemActivitymainBinding>(inflater, R.layout.item_activitymain, parent, false)
// We set the lifecycle owner here
binding.setLifecycleOwner(lifecycleOwner)
return ResultViewHolder(binding)
}
...
}
// In your activity/fragment, pass the view as a parameter when creating the adapter
adapter = ResultAdapter(this)
(In the adapter, I have removed the property mBinding and the constructor parameter context, as neither of them were necessary.)
Second, you are defining the property movie in your layout, but you are not setting it with an actual value. To fix this, you have to update your implementation of onBindViewHolder():
override fun onBindViewHolder(holder: ResultViewHolder, position: Int) {
val movie = getItem(position)
// Here we set the layout variable "movie" with its corresponding value
holder.itemActivitymainBinding.movie = movie
}
(Please note that here I have removed the code you had written to change the title of your textview because you should change it through data-binding in the layout by doing this: android:text="#{movie.mTitle}".)
With these changes, your implementation should hopefully work!
I created my own layout. And I defined a "good" attribute to use for its child views.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyLayout">
<attr name="good" format="boolean" />
</declare-styleable>
</resources>
The attribute is used like this. It is sort of like you can use android:layout_centerInParent for child views of a RelativeLayout, although I am not sure why mine should start with "app:" and that starts with "android:".
<?xml version="1.0" encoding="utf-8"?>
<com.loser.mylayouttest.MyLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<Button
app:good = "true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</com.loser.mylayouttest.MyLayout>
Now I want to read that attribute from children. But how? I have searched the web and tried a few things, but it did not seem to work.
class MyLayout: LinearLayout
{
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int)
{
setMeasuredDimension(200, 300); // dummy
for(index in 0 until childCount)
{
val child = getChildAt(index);
val aa = child.context.theme.obtainStyledAttributes(R.styleable.MyLayout);
val good = aa.getBoolean(R.styleable.MyLayout_good, false)
aa.recycle();
Log.d("so", "value = $good")
}
}
}
Added: With the comment as a hint, I found this document, and modified my code like below, and now I get the result I wanted.
class MyLayout: LinearLayout
{
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int)
{
setMeasuredDimension(200, 300);
for(index in 0 until childCount)
{
val child = getChildAt(index);
val p = child.layoutParams as MyLayoutParams;
Log.d("so", "value = ${p.good}")
}
}
override fun generateDefaultLayoutParams(): LayoutParams
{
return MyLayoutParams(context, null);
}
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams
{
return MyLayoutParams(context, attrs);
}
override fun checkLayoutParams(p: ViewGroup.LayoutParams?): Boolean
{
return super.checkLayoutParams(p)
}
inner class MyLayoutParams: LayoutParams
{
var good:Boolean = false;
constructor(c: Context?, attrs: AttributeSet?) : super(c, attrs)
{
if(c!=null && attrs!=null)
{
val a = c.obtainStyledAttributes(attrs, R.styleable.MyLayout);
good = a.getBoolean(R.styleable.MyLayout_good, false)
a.recycle()
}
}
}
}