Passing dynamic attribute to a view in a layout XML - android

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)

Related

Set the style programmatically

I created a custom TabLayout for my viewpage 2 and everything works fine. But I don't know how to programmatically make the text of the tabs lowercase (except for the first letter). I found a way that you can create your own style and set textAllCaps = false and pass it to the constructor. I did as follows:
class CustomTabLayout : TabLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
R.style.m_tab_layout_style
)
}
And I call so:
val mTabLayout = CustomTabLayout(this, null, R.style.m_tab_layout_style)
But it doesn't work and I found information that starting from android 5 it is not possible to pass a style after constructing a view. And the correct way to solve this problem is to pass to a 4-argument constructor, for example,
RelativeLayout r = new RelativeLayout(this, null, 0, R.style.MyRelativeLayout);
But TabLayout does not have a fourth defStyleRes argument, and who can tell me how to pass the style for TabLayout programmatically or how to make the TabLayout tab text lowerCase programmatically (except for the first letter).

Espresso not finding text inside of Custom View

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

ConstraintLayout view binding migration from Kotlin synthetics

I have an existing view that extends from ConstraintLayout which looks something like this:
class LandingTemplate: ConstraintLayout {
init {
inflate(context, R.layout.landing_template, this)
// Currently this 'recyclerView' is a kotlin synthetic
recyclerView.run {
// this sets up the recycler view
}
}
I'm familiar with view binding with activities and fragments, but I can't find any documentation around the extends layout case.
My question is, what do I replace that initial inflate call with here?
I'm assuming you have a context available from your constructor and your XML layout's top level tag is <merge>. You can use your binding class's inflate to create and add the child layout.
And since this can all be set up in the constructor, you don't need lateinit var like in the Activity/Fragment examples, and can just use val instead.
class LandingTemplate(context: Context, attrs: AttributeSet): ConstraintLayout(context, attrs) {
private val binding = LandingTemplateBinding.inflate(LayoutInflater.from(context), this)
init {
binding.recyclerView.run {
// this sets up the recycler view
}
}
}
you can get layout inflater like below
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val view = inflater.inflate(R.layout.landing_temple,this,true)
and you must have valid view construct too
LandingTemple(Context) // for creating view programmatically
LandingTemple(Context,AttrributeSet) // to inflate view from xml , and
//the constructor context is one that you use to call `getSystemService
for more information check

Remove LinearLayout with a button, which is inside this LinearLayout

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

How to extend ImageView in an Android-Scala app?

I have tried many solutions found in google by the keywords: multiple constructors, scala, inheritance, subclasses.
None seems to work for this occasion. ImageView has three constructors:
ImageView(context)
ImageView(context,attribute set)
ImageView(context,attribute set, style)
In scala you can only extend one of them. And the solution of using the more complete constructor (ImageView(context,attribute set, style)) and passing default values does not work either because the constructor ImageView(context) does something completely different than the other two constructors.
Some solutions of using a trait or a companion object does not seem to work because the CustomView must be a class! I mean I am not the only one who uses this class (so I could write the scala code any way I wanted) there is also the android-sdk who uses this class and yes it must be a class.
target is to have a CustomView which extends ImageView and all of these work:
new CustomView(context)
new CustomView(context,attribute set)
new CustomView(context,attribute set, style)
Please let me know if you need any further clarification on this tricky matter!
According to Martin Odersky (the creator of Scala), that is not possible.
In http://scala-programming-language.1934581.n4.nabble.com/scala-calling-different-super-constructors-td1994456.html:
"is there a way to call different super-constructors within different
class-constructors - or does all have to go up to the main-constructor and only
one super-constructor is supported?
No, it has to go through the main constructor. That's one detail where
Scala is more restrictive than Java."
I think your best approach is to implement your views in Java.
It sounds like you may just be better off writing the subclass in Java. Otherwise, if your assumption about the SDK using the three argument constructor is correct, then you could use a trait and a class with a companion object. The SDK would use the three argument constructor of CustomView which also implements a trait containing any additional behavior you need:
trait TCustomView {
// additional behavior here
}
final class CustomView(context: Context, attributes: AttributeSet, style: Int)
extends ImageView(context, attributes, style) with TCustomView
In the application code, you could use the one, two or three argument version in this way:
object CustomView {
def apply(c: Context, a: AttributeSet, s: Int) =
new ImageView(c, a, s) with TCustomView
def apply(context: Context, attributes: AttributeSet) =
new ImageView(context, attributes) with TCustomView
def apply(context: Context) = new ImageView(context) with TCustomView
}
CustomView(context)
CustomView(context, attributes)
CustomView(context, attributes, style)
Seems like a lot of work. Depending on your goals, you might be able to add additional behavior with implicits:
implicit def imageViewToCustomView(view: ImageView) = new {
def foo = ...
}
This question lead me to several design considerations which I would like to share with you.
My first consideration is that if a Java class has been correctly designed the availability of multiple constructors should be a sign of the fact that some class properties might have default value.
Scala provides default values as a language feature, so that you do not have to bother about providing multiple constructors at all, you can simply provide default value for some arguments of your constructor. This approach leads to a much cleaner API than three different constructors which produce this kind of behaviour as you are making clear why you do not need to specify all the parameters.
My second consideration is that this is not always applicable in case you are extending classes of a third-party library. However:
if you are extending a class of an open source library and the class is correctly designed, you can simply investigate the default values of the constructor argument
if you are extending a class which is not correctly designed (i.e. where overloaded constructors are not an API to default some parameters) or where you have no access to the source, you can replace inheritance with composition and provide an implicit conversion.
class CustomView(c: Context, a: Option[AttributeSet]=None, s: Option[Int]=None){
private val underlyingView:ImageView = if(a.isDefined)
if (s.isDefined)
new ImageView(c,a.get,s.get)
else
new ImageView(c,a.get)
else
new ImageView(c)
}
object CustomView {
implicit def asImageView(customView:CustomView):ImageView = customView.underlyingView
}
According to the Android View documentation View(Context) is used when constructed from code and View(Context, AttributeSet) and View(Context, AttributeSet, int) (and since API level 21 View(Context, AttributeSet, int, int)) are used when the View is inflated from XML.
The XML constructor all just call the same constructor, the one with the most arguments which is the only one with any real implementation, so we can use default arguments in Scala. The "code constructor" on the other hand may have another implementation, so it is better to actually call in from Scala as well.
The following implementation may be a solution:
private trait MyViewTrait extends View {
// implementation
}
class MyView(context: Context, attrs: AttributeSet, defStyle: Int = 0)
extends View(context, attrs, defStyle) with MyViewTrait {}
object MyView {
def apply(context: Context) = new View(context) with MyViewTrait
}
The "code constructor" may then be used like:
var myView = MyView(context)
(not a real constructor).
And the other once like:
var myView2 = new MyView(context, attrs)
var myView3 = new MyView(context, attrs, defStyle)
which is the way the SDK expects them.
Analogously for API level 21 and higher the class can be defined as:
class MyView(context: Context, attrs: AttributeSet, defStyle: Int = 0, defStyleRes: Int = 0)
extends View(context, attrs, defStyle, defStyleRes) with MyViewTrait {}
and the forth constructor can be used like:
var myView4 = new MyView(context, attrs, defStyle, defStyleRes)
Update:
It gets a bit more complicated if you try to call a protected method in View, like setMeasuredDimension(int, int) from the trait. Java protected methods cannot be called from traits. A workaround is to implement an accessor in the class and object implementations:
private trait MyViewTrait extends View {
protected def setMeasuredDimensionAccessor(w: Int, h: Int): Unit
def callingSetMeasuredDimensionAccessor(): Unit = {
setMeasuredDimensionAccessor(1, 2)
}
}
class MyView(context: Context, attrs: AttributeSet, defStyle: Int = 0)
extends View(context, attrs, defStyle) with MyViewTrait {
override protected def setMeasuredDimensionAccessor(w: Int, h: Int) =
setMeasuredDimension(w, h)
}
object MyView {
def apply(context: Context) = new View(context) with MyViewTrait {
override protected def setMeasuredDimensionAccessor(w: Int, h: Int) =
setMeasuredDimension(w, h)
}
}

Categories

Resources