I am extending a RelativeLayout to make a fragment decorator.
Like this:
<br.com.simplepass.mapfragmentwrapper.MapFragmentWrapper
android:id="#+id/map_wrapper"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="#+id/main_fragment_map"
android:layout_width="match_parent"
android:layout_height="match_parent"
class="com.google.android.gms.maps.MapFragment"/>
</br.com.simplepass.mapfragmentwrapper.MapFragmentWrapper>
Well, I would like to add some images in the RelativeLayout so it always get on top of the fragment, so I did this:
class MapFragmentWrapper : RelativeLayout {
var mMarkImageView : ImageView? = null
var mShadowView : View? = null
constructor(context: Context) : super(context) {
init(context)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(context)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(context)
}
#TargetApi(23)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
init(context)
}
private fun init(context: Context) {
val params = RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT)
params.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
mMarkImageView = ImageView(context)
mMarkImageView?.setImageResource(R.drawable.ic_marker_centered)
mShadowView = View(context)
mShadowView?.setBackgroundResource(R.drawable.map_pin_shadow)
addView(mMarkImageView, params)
addView(mShadowView, params)
}
[some more code here...]
}
But, when my activity starts, the fragment gets on top of my added views (mMarkImageView and mShadowView)... and that's exactly the opposite of what a want.
So how do I programmatically put the views at the last position in viewgroups?
Any help is appreciated!
So I found the answer:
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
super.onLayout(changed, l, t, r, b)
removeView(mMarkImageView)
removeView(mShadowView)
addView(mMarkImageView, -1 , params)
addView(mShadowView, -1, params)
}
doing this I put the layouts at the end of the viewgroup
Related
I want to implement an OnLongClickListener to some of my TextViews, but I don't want to repeat the same code everywhere, so I want to extend TextView and implement the OnLongClickListener just once.
class LongClickToCopyTextView : TextView {
constructor(context: Context) : this(context, null, 0)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
init {
setOnLongClickListener {
val clipboard = context?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
val clip = ClipData.newPlainText(context?.packageName, text)
clipboard?.primaryClip = clip
true
}
}
}
The implementation of the listener is used copy the text of the TextView into the clipboard when a user long presses it.
The problem is the text of the custom TextView is not shown. But if I use regular TextView the text is displayed correctly.
XML
<com.dzboot.myips.custom.LongClickToCopyTextView
android:id="#+id/simNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:text="00"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
The issue with setting default parameters for defStyleAttr is, that the base class might do the same to actually handle styles and states. Your initialisation happens in init {} anyhow.
class LongClickToCopyTextView : TextView {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
init {
setOnLongClickListener {
val clipboard = context?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
val clip = ClipData.newPlainText(context?.packageName, text)
clipboard?.primaryClip = clip
true
}
}
}
Also you might want to extend fro AppCompatTextView instead. It has some newer features backported.
I want to create a base class for all of my custom views. It can be a different type, for example, RelativeLayout or NavigationView.
So I've created abstract class with generics which is implementing the deepest class of these views which is connecting them - View. This is what I've got:
abstract class MyCustomView<VS : ViewState, V : View<VS>, P : Presenter<VS, V, *>>(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) { ... }
But the problem is that now I'm able to inherit only from the View. How can I build somehow generic that must be a child of the View so I could implement it and still to be able inside my base class to override methods like onAttachedToWindow?
Try to declare your class like this:
abstract class MyCustomView<VS : ViewState, V : View<VS>, P : Presenter<VS, V, *>>() : View{
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
(...)
}
I have encountered a problem to which I have been so laboring for at least 2 weeks now and I felt so dumbfounded that after so many years I kind of forgot how databinding works and how to correctly set it up for "CUSTOM VIEWS". I decided to check it out on a very simple project to isolate it from my current project. A very simple HelloWorld app which basically outputs Hello World to the screen using Data Binding. The project contains the following files:
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(LayoutInflater.from(this))
setContentView(binding.root)
binding.message = "Hello World!"
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="message" type="String" />
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.neonapps.android.sample.databinding.CustomView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
<!-- Please take note I am data binding on my custom view -->
app:message="#{message}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</android.support.constraint.ConstraintLayout>
</layout>
And now here is the most important part of the problem. This is a custom view CustomView.I want to bind a particular data 'String' into this view, as such it is able to output "Hello World" on this CustomView:
class CustomView(context : Context, attrs : AttributeSet, defStyleAttrs : Int, defStylRes : Int) : RelativeLayout(context){
constructor(context : Context, attrs : AttributeSet) : this(context, attrs, 0, 0)
constructor(context : Context, attrs : AttributeSet, defStyleAttrs : Int) : this(context, attrs, defStyleAttrs, 0)
private var myMessage : String? = null
set(value){
value.let{
field = it
binding.message = field
}
}
private val binding : LayoutCustomViewBinding = LayoutCustomViewBinding.inflate(LayoutInflater.from(context), this, true)
init {
binding.message?.let{
binding.message = it
}
}
fun setMessage(message : String?){
myMessage = message
}
}
#BindingAdapter(value = ["message"])
fun setMessage(view : TextView, message : String?)
{
message?.let{
view.text = it
}
}
#BindingAdapter(value = ["message"])
fun setMessage(view : CustomView, message : String?)
{
message?.let{
view.message = it
}
}
Here is the catch. This CustomView inflates a view which can be binded:
<?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"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable name="message" type="String" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:message="#{message}"
tools:text="Hello World"/>
</RelativeLayout>
</layout>
So I basically binding a String onto this custom view (which is composed (supposedly) of many views in its layout) once I set it from outside, like the activity_main.xml above.
activity_main.kt
<layout
...>
<data>
...
</data>
<android.support.constraint.ConstraintLayout
...>
<com.neonapps.android.sample.databinding.CustomView
...
<!-- Please take note I am data binding on my custom view -->
app:message="#{message}"
.../>
</android.support.constraint.ConstraintLayout>
</layout>
Once I build the entire project, everything seems to work fine. I run now the app and I am getting the following error:
Attempt to invoke virtual method 'void ******.databinding.CustomView.setTag(java.lang.Object)' on a null object reference
at com.neonapps.android.sample.databinding.databinding.ActivityMainBindingImpl.<init>(ActivityMainBindingImpl.java:37)
at com.neonapps.android.sample.databinding.databinding.ActivityMainBindingImpl.<init>(ActivityMainBindingImpl.java:29)
at com.neonapps.android.sample.databinding.DataBinderMapperImpl.getDataBinder(DataBinderMapperImpl.java:44)
at android.databinding.MergedDataBinderMapper.getDataBinder(MergedDataBinderMapper.java:74)
at android.databinding.DataBindingUtil.bind(DataBindingUtil.java:199)
at android.databinding.DataBindingUtil.inflate(DataBindingUtil.java:130)
at com.neonapps.android.sample.databinding.databinding.ActivityMainBinding.inflate(ActivityMainBinding.java:49)
at com.neonapps.android.sample.databinding.databinding.ActivityMainBinding.inflate(ActivityMainBinding.java:43)
at *****.MainActivity.onCreate(MainActivity.kt:12)
at android.app.Activity.performCreate(Activity.java:6904)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1136)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3266)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3415)
at android.app.ActivityThread.access$1100(ActivityThread.java:229)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1821)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:7406)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1230)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1120)
My app crashes, all of sudden, I went into panic. I just don't know the basics of DataBinding anymore. It works great when I just databinding on views but I am having no luck at all data binding on my own custom views.One thing that is driving me crazy is that this crashes on a code that is autogenerated. I have absolutely no idea how it generated a code referencing on a null it generated without assigning a reference to it. I surrender, there is something I missed badly.
I definitely missed something and I cannot seem to spot it. I kept cross referencing the DataBinding library docs but nothing comes up to me useful.
I tried this code on
Android Studio: 3.4 Canary 7
Kotlin: 1.3.11
Android Studio: 3.2.1
Kotlin: 1.2.71
First I thought it might be Kotlin/Build config/gradle related problem, until I build this project on stable environments and they behave the same regardless.
This is my curse. Any help to lessen my suffering would be appreciated!
This is my curse. Any help to lessen my suffering would be appreciated!
I'll try.
Remove the two BindingAdapters you wrote and rewrite your class to this:
class CustomView : RelativeLayout {
constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0, 0)
constructor(context: Context, attrs: AttributeSet, defStyleAttrs: Int) :
this(context, attrs, defStyleAttrs, 0)
constructor(context: Context, attrs: AttributeSet, defStyleAttrs: Int, defStylRes: Int) :
super(context, attrs, defStyleAttrs, defStylRes)
private val binding: LayoutCustomViewBinding = LayoutCustomViewBinding.inflate(LayoutInflater.from(context), this, true)
var message: String? = null
set(value) {
binding.message = value
}
}
Replace the app:message="#{message}" part in the layout of your CustomView with android:text="#{message}":
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable name="message" type="String" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{message}"
tools:text="Hello World"/>
</RelativeLayout>
</layout>
Explanation:
You will not need the BindingAdapters that you wrote as the databinding library automatically detects the setMessage() method that is generated from the message field in your Kotlin class during compilation. The same holds for the TextView, the databinding library will detect that there is a setText() method and use it for android:text="#{message}".
This is a bit of a late answer but might help others facing similar issues. I ran into the same issue while developing a customview that was inflating its own layouts for phone/tablet and adding them to the view hierarchy. For me, turns out the issue was using the generated Binding object to inflate the layout inside this custom view. It all had to do with a function of ViewDataBinding.java which recursively goes through the view hierarchy and gets the binding objects for each child.
private static void mapBindings(DataBindingComponent bindingComponent, View view,
Object[] bindings, IncludedLayouts includes, SparseIntArray viewsWithIds,
boolean isRoot) {
final int indexInIncludes;
final ViewDataBinding existingBinding = getBinding(view);
if (existingBinding != null) {
return;
}
...
}
The return statement was causing the mapping to exit out from the recursive function too early and my main binding class's views were null hence the crash.
Here's my custom view for reference:
class CustomContainerView : LinearLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs,
defStyleAttr)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int,
defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
var binding: CustomContainerViewBinding? = null
val customViewRoot: View
init {
orientation = VERTICAL
customViewRoot= LayoutInflater.from(context)
.inflate(R.layout.custom_container_view, this, false)
addView(customViewRoot)
if (!isInEditMode) {
binding = CustomContainerViewBinding.bind(customViewRoot)
}
}
private fun relocate(child: View) {
removeView(child)
val params = child.layoutParams ?: ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT)
findViewById<ViewGroup>(R.id.contentContainer)
.addView(child, params)
}
override fun onViewAdded(child: View?) {
child?.let {
if (it != customViewRoot) {
relocate(it)
}
}
}
}
So for me the culprit was this line:
if (!isInEditMode) {
binding = CustomContainerViewBinding.bind(customViewRoot)
}
To fix it, I just remove the above code from the init block and moved it to a lazy load getter.
class CustomContainerView : LinearLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs,
defStyleAttr)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int,
defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
private var customContainerBinding: CustomContainerViewBinding? = null
val customViewRoot: View
init {
orientation = VERTICAL
customViewRoot= LayoutInflater.from(context)
.inflate(R.layout.custom_container_view, this, false)
addView(customViewRoot)
}
private fun relocate(child: View) {
removeView(child)
val params = child.layoutParams ?: ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT)
findViewById<ViewGroup>(R.id.contentContainer)
.addView(child, params)
}
override fun onViewAdded(child: View?) {
child?.let {
if (it != customViewRoot) {
relocate(it)
}
}
}
fun getCustomContainerBinding(): CustomContainerViewBinding? {
if (customContainerBinding == null) {
customContainerBinding = CustomContainerViewBinding.bind(customViewRoot)
}
return customContainerBinding
}
}
The same thing could be happening for OP's question because it looks like you're relying on your Binding class to inflate your layout.
private val binding : LayoutCustomViewBinding = LayoutCustomViewBinding.inflate(LayoutInflater.from(context), this, true)
So to fix it, just follow the regular approach of inflating LayoutInflater.inflate() to create your view, then create a getter function that can lazy bind to the Binding object.
class CustomView(context : Context, attrs : AttributeSet, defStyleAttrs : Int, defStylRes : Int) : RelativeLayout(context){
constructor(context : Context, attrs : AttributeSet) : this(context, attrs, 0, 0)
constructor(context : Context, attrs : AttributeSet, defStyleAttrs : Int) : this(context, attrs, defStyleAttrs, 0)
private var myMessage : String? = null
set(value){
value.let{
field = it
binding?.message = field
}
}
private val rootView : View
private val binding : LayoutCustomViewBinding?
get() = LayoutCustomViewBinding.bind(rootView)
init {
rootView = LayoutInflater.from(context)
.inflate(R.layout.layout_custom_view, this, true)
}
fun setMessage(message : String?){
myMessage = message
}
}
#BindingAdapter(value = ["message"])
fun setMessage(view : TextView, message : String?)
{
message?.let{
view.text = it
}
}
#BindingAdapter(value = ["message"])
fun setMessage(view : CustomView, message : String?)
{
message?.let{
view.message = it
}
}
Must have all constructors.
Must call the two super constructors.
Like this:
public CustomView(Context context) {
super(context);
}
public CustomView(Context context, #Nullable AttributeSet attrs) {
this(context,attrs, 0);
}
public CustomView(Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public CustomView(Context context, #Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
I have a custom layout as below
class CustomComponent : FrameLayout {
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
initAttrs(attrs)
}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
initAttrs(attrs)
}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
initAttrs(attrs)
}
init {
LayoutInflater.from(context).inflate(R.layout.view_custom_component, this, true)
}
fun initAttrs(attrs: AttributeSet?) {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.custom_component_attributes, 0, 0)
my_title.text = resources.getText(typedArray
.getResourceId(R.styleable.custom_component_attributes_custom_component_title, R.string.component_one))
typedArray.recycle()
}
}
Now for each constructor, I have to explicitly call initAttrs(attrs) as I can't find way to access attrs in my init function.
Is there a way I could access attrs in init function, so I could call initAttrs(attrs) from init instead of having to explicitly call it in each of the constructor?
Use a constructor with default arguments:
class CustomComponent #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : FrameLayout(context, attrs, defStyle) {
fun init {
// Initialize your view
}
}
The #JvmOverloads annotation tells Kotlin to generate three overloaded constructors so they can be called in Java as well.
In your init function, attrs becomes available as a nullable type:
fun init {
LayoutInflater.from(context).inflate(R.layout.view_custom_component, this, true)
attrs?.let {
val typedArray = context.obtainStyledAttributes(it, R.styleable.custom_component_attributes, 0, 0)
my_title.text = resources.getText(typedArray
.getResourceId(R.styleable.custom_component_attributes_custom_component_title, R.string.component_one))
typedArray.recycle()
}
}
Note the usage of it in the let block.
I'm trying to use Kotlin in my Android project. I need to create custom view class. Each custom view has two important constructors:
public class MyView extends View {
public MyView(Context context) {
super(context);
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
MyView(Context) is used to instantiate view in code, and MyView(Context, AttributeSet) is called by layout inflater when inflating layout from XML.
Answer to this question suggests that I use constructor with default values or factory method. But here's what we have:
Factory method:
fun MyView(c: Context) = MyView(c, attrs) //attrs is nowhere to get
class MyView(c: Context, attrs: AttributeSet) : View(c, attrs) { ... }
or
fun MyView(c: Context, attrs: AttributeSet) = MyView(c) //no way to pass attrs.
//layout inflater can't use
//factory methods
class MyView(c: Context) : View(c) { ... }
Constructor with default values:
class MyView(c: Context, attrs: AttributeSet? = null) : View(c, attrs) { ... }
//here compiler complains that
//"None of the following functions can be called with the arguments supplied."
//because I specify AttributeSet as nullable, which it can't be.
//Anyway, View(Context,null) is not equivalent to View(Context,AttributeSet)
How can this puzzle be resolved?
UPDATE: Seems like we can use View(Context, null) superclass constructor instead of View(Context), so factory method approach seems to be the solution. But even then I can't get my code to work:
fun MyView(c: Context) = MyView(c, null) //compilation error here, attrs can't be null
class MyView(c: Context, attrs: AttributeSet) : View(c, attrs) { ... }
or
fun MyView(c: Context) = MyView(c, null)
class MyView(c: Context, attrs: AttributeSet?) : View(c, attrs) { ... }
//compilation error: "None of the following functions can be called with
//the arguments supplied." attrs in superclass constructor is non-null
Kotlin supports multiple constructors since M11 which was released 19.03.2015. The syntax is as follows:
class MyView : View {
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) {
// ...
}
constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0) {}
}
More info here and here.
Edit: you can also use #JvmOverloads annotation so that Kotlin auto-generates the required constructors for you:
class MyView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : View(context, attrs, defStyle)
Beware, though, as this approach may sometimes lead to the unexpected results, depending on how the class you inherit from defines its constructors. Good explanation of what might happen is given in that article.
You should use annotation JvmOverloads (as it looks like in Kotlin 1.0), you can write code like this:
class CustomView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : View(context, attrs, defStyle)
This will generate 3 constructors just as you most likely wanted.
Quote from docs:
For every parameter with a default value, this will generate one
additional overload, which has this parameter and all parameters to
the right of it in the parameter list removed.
Custome View with kotlin here's sample code.
class TextViewLight : TextView {
constructor(context: Context) : super(context) {
val typeface = ResourcesCompat.getFont(context, R.font.ccbackbeat_light_5);
setTypeface(typeface)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
val typeface = ResourcesCompat.getFont(context, R.font.ccbackbeat_light_5);
setTypeface(typeface)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
val typeface = ResourcesCompat.getFont(context, R.font.ccbackbeat_light_5);
setTypeface(typeface)
}
}
TL;DR most of the time, it should be enough to just define your custom view as:
class MyView(context: Context, attrs: AttributeSet?) : FooView(context, attrs)
Given this Java code:
public final class MyView extends View {
public MyView(Context context) {
super(context);
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
its Kotlin equivalent would use secondary constructors:
class MyView : View {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
}
That syntax is useful when you really want to call different super-class constructors depending on whether the view is created in code or inflated from XML. The only case that I know of for this to be true is when you are extending the View class directly.
You can use a primary constructor with default arguments and a #JvmOverloads annotation otherwise:
class MyView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs)
You don't need #JvmOverloads constructor if you don't plan to call it from Java.
And if you only inflate views from XML, then you can just go with the simplest:
class MyView(context: Context, attrs: AttributeSet?) : View(context, attrs)
If your class is open for extension and you need to retain the style of the parent, you want to go back to the first variant that uses secondary constructors only:
open class MyView : View {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
}
But if you want an open class that overrides the parent style and lets its subclasses override it too, you should be fine with #JvmOverloads:
open class MyView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.customStyle,
defStyleRes: Int = R.style.CustomStyle
) : View(context, attrs, defStyleAttr, defStyleRes)
This does seem to be an issue. I've never run into this because my custom views have either been created only in xml or only in code, but I can see where this would come up.
As far as I can see, there are two ways around this:
1) Use constructor with attrs. Using the view in xml will work fine. In code, you need to inflate an xml resource with the desired tags for your view, and convert it to an attribute set:
val parser = resources.getXml(R.xml.my_view_attrs)
val attrs = Xml.asAttributeSet(parser)
val view = MyView(context, attrs)
2) Use the constructor without attrs. You can't place the view directly in your xml, but it's easy about to place a FrameLayout in the xml and add the view to it through code.
There are several ways to override your constructors,
When you need default behavior
class MyWebView(context: Context): WebView(context) {
// code
}
When you need multiple version
class MyWebView(context: Context, attr: AttributeSet? = null): WebView(context, attr) {
// code
}
When you need to use params inside
class MyWebView(private val context: Context): WebView(context) {
// you can access context here
}
When you want cleaner code for better readability
class MyWebView: WebView {
constructor(context: Context): super(context) {
mContext = context
setup()
}
constructor(context: Context, attr: AttributeSet? = null): super(context, attr) {
mContext = context
setup()
}
}
Added a complete example of creating a custom view by inflating XML layout with multiple constructors
class MyCustomView : FrameLayout {
private val TAG = MyCustomView ::class.simpleName
constructor(context: Context): super(context) {
initView()
}
constructor(context: Context, attr: AttributeSet? = null): super(context, attr) {
initView()
}
constructor(
context: Context,
attrs: AttributeSet?,
defStyleAttr: Int
): super(context, attrs, defStyleAttr) {
initView()
}
/**
* init View Here
*/
private fun initView() {
val rootView = (context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater)
.inflate(R.layout.layout_custom_view, this, true)
// Load and use rest of views here
val awesomeBG= rootView.findViewById<ImageView>(R.id.awesomeBG)
}
in XML add your layout_custom_view view file
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="#+id/awesomeBG"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="#string/bg_desc"
android:fitsSystemWindows="true"
android:scaleType="centerCrop" />
<!--ADD YOUR VIEWs HERE-->
</FrameLayout>
It seems, constructor parameters are fixed by type and order, but we can add own like this:
class UpperMenu #JvmOverloads
constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0,parentLayout: Int,seeToolbar: Boolean? = false)
: Toolbar(context, attrs, defStyleAttr) {}
where parentLayout ,seeToolbar are added to it so :
val upper= UpperMenu (this,null,0,R.id.mainParent, true)
When you have some view (BottomSheetDialog) that already can show text and want to add formatted string, you should add two constructors.
class SomeDialog : BottomSheetDialog {
private val binding = DialogSomeBinding.inflate(layoutInflater)
// Base constructor that cannot be called directly
private constructor(
context: Context,
title: CharSequence
) : super(context) {
setContentView(binding.root)
binding.title.text = title
}
// Constructor with simple CharSequence message
constructor(
context: Context,
title: CharSequence,
message: CharSequence
) : this(context, title) {
binding.message.text = message
}
// Constructor with formatted SpannableString message
constructor(
context: Context,
title: CharSequence,
message: SpannableString
) : this(context, title) {
binding.message.text = message
}
}
Usage:
val span = SpannableString(getString(R.string.message, name))
...
SomeDialog(
context = requireContext(),
title = getString(R.string.title),
message = span
).show()
You can try new Library Anko for Kotlin from JetBrains (also you can contribute on github).
Currently it is in beta, but you can create views with such code
button("Click me") {
textSize = 18f
onClick { toast("Clicked!") }
}
Have a look at this library