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()
}
}
}
}
Related
The picture below shows my custom view.
My Custom Layout Image
I want add custom buttons in child layout in my custom view.
So, I tried like below.
My CustomView is a ConstraintLayout and the code is
class MyCustomLayout(
context: Context,
attributeSet: AttributeSet? = null
) : ConstraintLayout(context, attributeSet) {
private var binding: MyCustomLayoutBinding
init {
binding = HarusekkiBaseToolbarBinding.inflate(
LayoutInflater.from(context),
this,
true
)
}
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
child ?: return
if (child is HaruSekkiToolbarButton) {
binding.buttonContainer.addView(child, index, params)
} else {
super.addView(child, index, params)
}
}
}
Also, I wrote xml of fragment
<ConstraintLayout
.../>
<MyCustomLayout
...>
<MyCustomButton
android:id="#+id/myButton1
... />
<MyCustomButton
android:id="#+id/myButton2
... />
</MyCustomLayout>
</ConstraintLayout>
When I access MyCustomButton in Fragment Class like
binding.myButton1.do_somthing
it returns null.
How to I get MyCustomButton? Is addView method what I override wrong?
I've also changed the addView function in MyCustomLayout as follows.
private val childViews = mutableListOf<View>()
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
child ?: return
childViews.add(child)
if (child is HaruSekkiToolbarButton) {
binding.toolbarButtonContainer.addView(child, index, params)
} else {
super.addView(child, index, params)
}
}
override fun getChildCount(): Int {
return childViews.size
}
override fun getChildAt(index: Int): View {
return childViews[index]
}
override fun indexOfChild(child: View?): Int {
return childViews.indexOf(child)
}
However, it didn't work well with ClassCastException.
(java.lang.ClassCastException: androidx.constraintlayout.widget.ConstraintLayout$LayoutParams cannot be cast to android.widget.LinearLayout$LayoutParams)
Plz Help me!
been at this for a while. Basically I want the views inside my custom ConstraintLayout to be centered programmatically.
My layout looks like this:
class CenterLayout #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs,defStyleAttr) {
private val constraintSet = ConstraintSet()
override fun onFinishInflate() {
super.onFinishInflate()
for (i in 0 until childCount) {
val child = getChildAt(i)
setConstraints(child)
}
constraintSet.applyTo(this)
}
private fun setConstraints(view:View){
constraintSet.clone(this)
constraintSet.centerHorizontally(view.id,ConstraintSet.PARENT_ID)
constraintSet.centerVertically(view.id,ConstraintSet.PARENT_ID)
}
}
Basically I loop through the children and apply some consstraints, but it doesn't seem to be working. The views are still anchored top left.
Any ideas?
Try accesing the layout params of the child view:
class Test(context: Context) : ConstraintLayout(context, null, 0) {
private fun setConstraints(child: View) {
val params = child.layoutParams as ConstraintLayout.LayoutParams
params.startToStart = ConstraintLayout.LayoutParams.PARENT_ID
params.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID
child.layoutParams = params
}
override fun addView(child: View?) {
super.addView(child)
child?.let { setConstraints(it) }
}
}
Also note how i override the addView method for get the child view added to the layout.
Ignore my constructor just take importance on the methods.
Note: I have not tested this code.
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()
}
})
}
}
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).
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}"
....
/>