How to center children in custom constraint layout programmatically - android

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.

Related

How to access child view in layout in Custom View Group?

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!

MotionLayout not recyclable as a child of RecyclerView

i try to implement programmatically version of MotionLayout by extending it. And i have a base activity ayout using RecyclerView.
However, when i add my motion layout as an item of the RecyclerView, the view is not recycled when i try to scrolling up and down.
And it works well when i use as a normal view (act as single view).
Here is the preview:
class SimpleMotionLayout #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : MotionLayout(context, attrs, defStyleAttr) {
private val motionScene = MotionScene(this)
private var _simpleTransition: MotionScene.Transition? = null
private lateinit var squareView: View
init {
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
initDefaultConstraint(this)
setMotion()
}
fun setMotion() {
_simpleTransition = createPlaceholderTransition(motionScene)
setDebugMode(DEBUG_SHOW_PATH)
/**
* The order matters here.
* [MotionScene.addTransition] adds the transition to the scene while
* [MotionScene.setTransition] sets the transition to be the current transition.
*/
motionScene.addTransition(_simpleTransition)
motionScene.setTransition(_simpleTransition)
scene = motionScene
setTransition(_simpleTransition!!.id)
animateView()
}
fun setSquareColor(color: Int) {
squareView.setBackgroundColor(color)
}
fun initDefaultConstraint(motionLayout: ConstraintLayout) {
// View
squareView = View(context).apply {
id = R.id.default_button
setBackgroundColor(Color.BLACK)
}
motionLayout.addView(
squareView,
LayoutParams(
fromDp(context, 52),
fromDp(context, 52)
)
)
val set = ConstraintSet()
set.clone(motionLayout)
// Setup constraint set to TOP, LEFT to the Parent
set.connect(
squareView.id,
TOP,
PARENT_ID,
TOP
)
set.connect(
squareView.id,
START,
PARENT_ID,
START
)
set.applyTo(motionLayout)
}
private fun setToEnd() {
val endSet = getConstraintSet(_simpleTransition?.endConstraintSetId ?: return)
endSet.clear(R.id.default_button, START)
endSet.connect(
R.id.default_button,
END,
PARENT_ID,
END
)
}
fun animateView() {
setToEnd()
_simpleTransition?.setOnSwipe(
OnSwipe().apply {
dragDirection = DRAG_END
touchAnchorId = R.id.default_button
touchAnchorSide = SIDE_START
onTouchUp = ON_UP_AUTOCOMPLETE_TO_START
setMaxAcceleration(500)
}
)
setTransition(_simpleTransition!!.id)
}
// Placeholder transition??
fun createPlaceholderTransition(motionScene: MotionScene): MotionScene.Transition? {
val startSetId = View.generateViewId()
val startSet = ConstraintSet()
startSet.clone(this)
val endSetId = View.generateViewId()
val endSet = ConstraintSet()
endSet.clone(this)
val transitionId = View.generateViewId()
return TransitionBuilder.buildTransition(
motionScene,
transitionId,
startSetId, startSet,
endSetId, endSet
)
}
/**
* Get px from dp
*/
private fun fromDp(context: Context, inDp: Int): Int {
val scale = context.resources.displayMetrics.density
return (inDp * scale).toInt()
}
}
Below is my adapter:
class SimpleMotionLayoutAdapter : RecyclerView.Adapter<SimpleMotionLayoutAdapter.ViewHolder>() {
val items = mutableListOf<Int>() // colors
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
fun setColor(color: Int) {
(view as SimpleMotionLayout).setSquareColor(color)
}
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = SimpleMotionLayout(parent.context)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.setColor(items[position])
}
override fun getItemCount(): Int = items.size
companion object {
const val TYPE_NORMAL = 0
const val TYPE_EXCEPTIONAL = 1
}
}
Am i missing implementation?
Thank you
In general you need to cache and restore the state of the MotionLayout when it gets Recycled.
Right now in onBindViewHolder you only set the Color.
Remember RecyclerView keeps a only a screens worth (+ about 3) of ViewHolders and reuses them using onBindViewHolder
At minimum you need to set the Progress of the MotionLayout.
Due to differences in timing you may need to set the progress in an onPost

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

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}"
....
/>

Read custom layout attribute from child views in a custom layout

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()
}
}
}
}

Write a test cases for custom view

A custom view contains several attributes as property, to make this code testable than added unit test (RobolectricTestRunner).
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
inflateView()
bindView()
initialize()
setContentHeight()
}
}
private fun inflateView() {
View.inflate(context, R.layout.view_custom, this)
}
private fun bindView() {
panelView = findViewById(R.id.panel_view)
}
private fun setContentHeight(height: Int) {
layoutParams.height = height <-- test case fail due to layoutParams null.
requestLayout()
}
TestCase
#Test
fun init() {
val activity = Robolectric.buildActivity(Activity::class.java).create().get()
var context = mock(Context::class.java)
var typedArray = mock(TypedArray::class.java)
var dropDownView = DropdownTextView(activity, mock(AttributeSet::class.java))
}
layoutParams having a null object and mock not work here.

Categories

Resources