I want to create custom class which extends TextInputLayout and have sort of an access into its inner EditText, so I can set focus listener on it. Based on focus state and text in the EditText the error shows in TextInputLayout. Below is my current simple code where edit text is null. How can I achieve so have custom TextInputLayout that can communicate with its inner EditText ?
class CustomTextInputLayout : TextInputLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
init {
Log.e("_TEST", this.editText?.toString() ?: "null") // gives null
editText?.setOnFocusChangeListener { v, hasFocus ->
if (hasfocus.not()) {
val textToCheck = editText?.text.toString()
this.error = someValidationFunction(textToCheck)
}
}
}
}
<com.app.utility.CustomTextInputLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</com.app.utility.CustomTextInputLayout>
The editText isn't initialized in init method yet. First, the parent will be laid out then the child.
Please, check this method
onLayout(boolean changed, int left, int top, int right, int bottom)
Views lifecycle is very important if you want to create custom views or viewgroups.
https://developer.android.com/reference/android/view/ViewGroup.html
Related
Say I have a Custom View built from scratch that looks like this:
class CustomTextView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL_AND_STROKE
textSize = 48f
color = Color.BLUE
strokeWidth = 3f
}
override fun onDraw(canvas: Canvas?) {
canvas?.drawText("Text from Custom view", width / 2f, height / 2f, paint)
}
}
This is very simple drawing Text on Canvas. And in a fragment layout, I add a TextView and my CustomText view like the following:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="32dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Text from Text View" />
<com.example.testing.views.CustomTextView
android:layout_width="250dp"
android:layout_height="32dp"
android:layout_marginTop="10dp" />
</LinearLayout
My espresso test file looks like:
#RunWith(AndroidJUnit4::class)
class MyFragmentTest {
private lateinit var scenario: FragmentScenario<MyFragment>
#Before
fun setup() {
scenario = launchFragmentInContainer(themeResId = R.style.Theme_Testing)
scenario.moveToState(Lifecycle.State.STARTED)
}
#Test
fun testNormalTextView() { // -> PASSED
onView(withText("Text from Text View")).check(matches(isDisplayed()))
}
#Test
fun testCustomTextView() { // -> FAILED NoMatchingView Exception
onView(withText("Text from Custom View")).check(matches(isDisplayed()))
}
}
When I run the tests on my physical device, it passes only testNormalTextView but it fails on testCustomTextView. How do I make these Espresso test pass with Custom Views?
From the official docs, withText() viewMatcher works with Textviews.
Returns a matcher that matches TextView based on its text property value.
In your case your custom view is extending View class.
Following are two ways which i will suggest.
Make your custom view extend TextView. [If your requirement is to access only the view with the specific text regardless of it's id]
Use withId() viewMatcher instead of withText(), passing id of your customview given in xml layout. You need to give id to your custom view in xml. [If you want to check view with specific id, not with the text it holds]
In your xml
<com.example.testing.views.CustomTextView
android:id="#+id/my_custom_view"
android:layout_width="250dp"
android:layout_height="32dp"
android:layout_marginTop="10dp" />
In your testFunction
#Test
fun testCustomTextView() {
onView(withId(R.id.my_custom_view)).check(matches(isDisplayed()))
}
Update:
For recyclerview, you can use onData() instead of onView() passing matcher in argument.
You can find further info about testing adapterViews here
I'm developing a custom search view and I need to add a listener so the viewmodel can perform the search with the query using databinding, I'm currently having issues setting up the binding adapter with the query parameter, here are the relevant parts:
This is the custom view with the listener event:
class MySearchView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = android.R.attr.editTextStyle
) : AppCompatEditText(context, attrs, defStyleAttr) {
var onSearchListener: ((query: String) -> Unit)? = null
}
This is the binding adapter:
#BindingAdapter("onSearchListener")
fun MySearchView.setOnSearchListener(event: (query: String) -> Unit) {
onSearchListener = event
}
This is the XMl layout part:
<com.xxx.MySearchView
android:id="#+id/txt_search"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="#string/label_search"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:onSearchListener="#{(query) -> viewModel.onSearchTriggered(query)}"/>
Finally my method in the viewmodel:
fun onSearchTriggered(query: String) {
Log.d("search", "search: $query")
}
When I compile the XML fails telling me that it cannot find the method above, here is the error:
cannot find method onSearchTriggered(java.lang.Object) in class com.xxx.MyViewModel
I've tried before with custom listeners without parameters and everything worked just fine so I assume I must be doing something wrong when adding the parameter, any ideas?
I do not think that would work as the type of parameter cannot be specified in XML, but I was hoping passing viewModel::onSearchTriggered would work but it looks like it does not. You could use one of the following to do this:
You can declare a variable instead of a function in the ViewModel:
val onSearchTriggered = { query: String ->
Log.d("search", "search: $query")
}
You can then directly pass the variable like so:
<com.xxx.MySearchView
android:id="#+id/txt_search"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="#string/label_search"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:onSearchListener="#{viewModel.onSearchTriggered}"/>
You could create an interface for your listener:
interface OnSearchListener {
fun onSearch(String)
}
#BindingAdapter("onSearchListener")
fun MySearchView.setOnSearchListener(listener: OnSearchListener) {
onSearchListener = listener::onSearch
}
You can then use it like so:
<com.xxx.MySearchView
android:id="#+id/txt_search"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="#string/label_search"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:onSearchListener="#{viewModel::onSearchTriggered}"/>
I've created a custom linear layout within android studio. This layout gets inflated into another vertical layout programatically. Now I want to map a button inside this layout, which can delete the whole object. Here is my layout:
And as you can see the button "DELETE HERE" should remove the 3 items, time, weekday and the button itself.
This is my class and here
class AlarmCard #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0,
defStyleRes: Int = 0,
) : LinearLayout(context, attrs, defStyle, defStyleRes) {
init {
LayoutInflater.from(context)
.inflate(R.layout.alarmcard, this, true)
btnDelete.setOnClickListener(){
**/* Call destructor or remove view !?!*/**
}
}
}
which get added to the linear layout with:
val monday = AlarmCard(this)
alarmCards.addView(monday)
The problem is for me how can I delete the object with a button? I tried using alarmCards.removeView(this) within btnDelete.setOnClickListener() but It crashes. :(
Thank you!!
Try this:
btnDelete.setOnClickListener {
(getParent() as? ViewGroup)?.removeView(this#AlarmCard)
}
Let's say the layout XML for my activity has a view like this. The XML below has a hard-coded value for the attribute, but I want to pass value from activity when setting the content view.
<myView
android:id = "#+id/myView
....
app:myAttriute = "someMode"/>
The view reads that value in the constructor like the following and sets the initial mode.
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
{
val inflater = context?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
inflater.inflate(R.layout.myView, this);
var arr = context.obtainStyledAttributes(attrs, R.styleable.myView);
var mode = arr.getString(R.styleable.myAttribute);
Is it possible to set that attribute in the activity so that the value can be used in the view's constructor? It may sound weird, it causes some problems to change the mode after the view's constructor like this
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val myView = findView...
myView.myAttribute = "someMode";
If possible, I wish I could pass the attribute to its constructor, somehow.
super.onCreate(savedInstanceState)
//takes the view's ID and attribute name/value to be used while
//setting the content view (passed to myView's constructor)
hypotheticalMethod(R.id.myView, "myAttriute", "someMode");
setContentView(R.layout.activity_main)
I'm trying to make a custom ProgressBar the way I did custom layouts a lot before. This time I'm having issues.
I can achieve the desired look with just this xml:
<ProgressBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminateOnly="false"
android:progressDrawable="#drawable/progress_bar" />
The ultimate goal, however, would be doing some of the customization in the custom class so the xml shrinks to this:
<se.my.viktklubb.app.progressbar.HorizontalProgressBar
android:id="#+id/planProgressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
Here is the custom class:
class HorizontalProgressBar : ProgressBar {
constructor(context: Context) : super(context) {
initialSetup()
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
initialSetup()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
initialSetup()
}
fun initialSetup() {
max = 100
isIndeterminate = false
progressDrawable = context.getDrawable(R.drawable.progress_bar)
}
}
Second constructor gets fired but the bar isn't styled. It actually appears as indeterminate spinning progress and none of these setup gets applied eventually - feels like those are overriden later by something else.
What's wrong here?
DISCLAIMER:
This is a very simple example. I'm perfectly aware I could go for styles or a simple xml implementation but I just use this simple case only to demonstrate the issue.
According to the bullet 5 of this article, you may need to modify initialSetup:
fun initialSetup() {
max = 100
isIndeterminate = false
progressDrawable = context.getDrawable(R.drawable.progress_bar)
invalidate()
requestLayout()
}
If it doesn't change anything, try calling initialSetup later, on onResume of activity/fragment for example. If this work, the problem is maybe linked to the complexity of initialization/lifecycle of android components.