How do I use selectableButtonBackground attribute on a custom View that uses Anko's apply() method inside its constructor like the following structure?
class XPTO(context: Context) : CardView(context) {
init {
this.apply {
// I'd like to invoke selectableButtonBackground here
}
}
I've tried to do context.obtainStyledAttributes(arrayOf(R.attr.selectableItemBackground).toIntArray()).getDrawable(0) but with no success.
I just created an extension function to get the resource ids for attributes.
val Context.selectableItemBackgroundResource: Int get() {
return getResourceIdAttribute(R.attr.selectableItemBackground)
}
fun Context.getResourceIdAttribute(#AttrRes attribute: Int) : Int {
val typedValue = TypedValue()
theme.resolveAttribute(attribute, typedValue, true)
return typedValue.resourceId
}
This way you can also add more attributes if needed. Example to put it in anko:
frameLayout {
textView {
text = "Test"
backgroundResource = selectableItemBackgroundResource
isClickable = true
}
}
Don't forget the isClickable, else you won't see anything when you're clicking the textView
Another way to achieve this with Anko:
val backgroundResource = attr(R.attr.selectableItemBackgroundBorderless).resourceId
Related
In the app I'm working on, it is imperative that all views in all layout files have an id set on them. So I'm trying to build a custom lint rule to enforce this.
Normally, one would use the getApplicableElements() method from XmlScanner and include a list of strings for each element tag. However, I can't seem to find a way to make this look at all elements in an XML layout which subclass View.
I tried using XmlScannerConstants.ALL, however, that looks at every single element in every single XML file. Given that we have several other types of XML-based resources, that's not going to work.
My code for the inspector class is below. Does anyone know of a good way filter getApplicableElements() so it looks at every element that subclasses View, and nothing else?
class IdDetector : ResourceXmlDetector() {
companion object {
private const val ISSUE_ID = "MissingId"
private const val ISSUE_DESCRIPTION = "Missing required attribute 'id'"
private const val ISSUE_EXPLANATION = "Identifiers are required on all views."
val ISSUE = Issue.create(
id = ISSUE_ID,
briefDescription = ISSUE_DESCRIPTION,
explanation = ISSUE_EXPLANATION,
category = Category.A11Y,
priority = 10,
severity = Severity.FATAL,
androidSpecific = true,
implementation = Implementation(IdDetector::class.java, Scope.RESOURCE_FILE_SCOPE)
)
}
override fun getApplicableElements(): Collection<String>? = XmlScannerConstants.ALL
override fun visitElement(context: XmlContext, element: Element) {
if (!element.hasAttributeNS("http://schemas.android.com/apk/res/android", "id")) {
context.report(ISSUE, element, context.getLocation(element), ISSUE_DESCRIPTION)
}
}
}
There's the appliesTo(#NonNull ResourceFolderType folderType) method that can help you achieve that. In your case, I believe you will be only interested in targeting the layout folder. You're new detector would look something like this:
class IdDetector : ResourceXmlDetector() {
companion object {
private const val ISSUE_ID = "MissingId"
private const val ISSUE_DESCRIPTION = "Missing required attribute 'id'"
private const val ISSUE_EXPLANATION = "Identifiers are required on all views."
val ISSUE = Issue.create(
id = ISSUE_ID,
briefDescription = ISSUE_DESCRIPTION,
explanation = ISSUE_EXPLANATION,
category = Category.A11Y,
priority = 10,
severity = Severity.FATAL,
androidSpecific = true,
implementation = Implementation(IdDetector::class.java, Scope.RESOURCE_FILE_SCOPE)
)
}
override fun appliesTo(folderType: ResourceFolderType): Boolean = ResourceFolderType.LAYOUT == folderType
override fun getApplicableElements(): Collection<String>? = XmlScannerConstants.ALL
override fun visitElement(context: XmlContext, element: Element) {
if (!element.hasAttributeNS("http://schemas.android.com/apk/res/android", "id")) {
context.report(ISSUE, element, context.getLocation(element), ISSUE_DESCRIPTION)
}
}
}
I have the following code on my RecyclerView:
class TaskViewHolder(v: View) : RecyclerView.ViewHolder(v), View.OnClickListener
{
var mId: TextView = v.task_id
var mDescription: TextView = v.task_description
var mCard: CardView = v.task_card
var mView: View = v
}
override fun onBindViewHolder(holder: TaskViewHolder, position: Int)
{
// Initialize ViewHolder content
holder.mId.text = items[position].getID().toString()
holder.mDescription.text = items[position].getDescription()
holder.mTask = items[position]
for (elem in priorities)
{
if (elem.getID() == items[position].getPriority())
{
holder.mCard.setCardBackgroundColor(Color.parseColor("#c2c2c2"))
break
}
}
}
I don't know why, but CardView backgroundColor isn't changing.
If I use the following code, it works correctly:
holder.mCard.setCardBackgroundColor(ContextCompat.getColor(holder.mView.context, R.color.priority3))
What I should do to set CardBackgroundColor progrmatically?
First of all, You need to provide else part as transparent or another color to avoid color duplicate render issue. second, you have to pass context from your activity or fragment to adapter and that context will be used to get color like below.
if (elem.getID() == items[position].getPriority())
{
holder.mCard.setCardBackgroundColor(ContextCompat.getColor(mContext, [first color]))
} else {
holder.mCard.setCardBackgroundColor(ContextCompat.getColor(mContext,[second color]))
}
Try replacing "#c2c2c2" with "#ffc2c2c2" to make sure you provide a correct alpha for the background colour. On android, this extra byte added at the beginning represents the alpha of the colour:
#ffc2c2c2
ff: alpha
c2: red
c2: green
c2: blue
I've been exploring android development using Anko and Kotlin and had some trouble with the ratingbar, namely it size. I've tried to make it smaller using a custom style but themedRatingBar doesn't seem to work. So I've opted to make a custom ratingbar instead. I can't seem to make it work that way I want it to in that when I set it this way in the main activity:
starRatingView{
setRating(3)
}
It does not output a rating of 3 and instead will output the default rating, which is zero.
class StarRatingView: _LinearLayout {
lateinit var imageViewStars: List<ImageView>
private var starNum: Float = 0f
private var starSize: Int = 5
constructor(context: Context): super(context) {
initializeView()
}
fun initializeView() {
with(this) {
linearLayout {
relativeLayout {
linearLayout {
for (i in 1..starSize)
imageView(R.drawable.ratingbar_empty)
}
linearLayout {
for (i in 0..Math.round(starNum)) {
imageView(R.drawable.ratingbar_filled)
}
}
}
}
}
}
fun setSize(starSize: Int){
this.starSize = starSize
}
fun setRating(starNum: Float){
this.starNum = starNum
}
}
Above is the code that I use to create the custom RatinBar. Trying to avoid using XMLs as much as possible and use Anko instead.
if I can help here is a possible solution to show a custom RatingBar in a custom Alert using Anko library and Kotlin code...
var rateGave: String? = null
alert {
title = "Rate your experience"
customView {
linearLayout {
ratingBar {
numStars = 5 //here is to define the number of stars you want
rating = 4f //starting rate to show as the alert pop up
setOnRatingBarChangeListener { ratingBar, rating, fromUser ->
rateGave = rating.toString()
}
}
}
}
positiveButton("Rate") { rate() }
}.show()
}
fun rate() {
println(rateGave) //now it's just printing out the rate to show that it's working fine, but in this function you can mange all the operations you need with the rating value
}
Hope it can be usefull.
Have a nice day :)
Here is my code. As you can see the body of apply is exactly the same. Is there a better than use extension function?
contentText?.let {
contentTextView?.apply {
visibility = View.VISIBLE
text = contentText
}
}
titleText?.let {
titleTextView?.apply {
visibility = View.VISIBLE
text = titleText
}
}
Here is my function
private fun setTextAndVisiblity(textView: TextView?, newText: String?): TextView? {
return textView?.apply {
visibility = View.VISIBLE
text = newText
}
}
This is my code when apply function
contentText?.let {
setVisibleText(contentTextView, it)
}
titleText?.let {
setVisibleText(titleTextView, it)
}
I would write the extension function like this:
fun TextView.setVisibleIfTextNotNull(text: CharSequence?) = text?.let {
visibility = View.VISIBLE
this.text = it
}
Usage:
contentTextView?.setVisibleIfTextNotNull(contentText)
titleTextView?.setVisibleIfTextNotNull(titleText)
You can either make it as an nested function or private extension function as you like. The name of the function may not be clear enough to clarify what the function does, you may think of a better one.
An extension function seems like the best choice. If you make the function return this you can use it without apply.
Your other choice would be to create an ordinary function and pass it into also using method references, e.g.
fun setVisibleText(view: View) { }
titleTextView.also(this::setVisibleText)
the cleanest for me is to declare an extension function as:
fun TextView.setVisibleWithText(text: String?){
text ?: return
visibility = View.VISIBLE
setText(text)
}
then calling it as:
myTextView?.setVisibleWithText(myText)
Anyway,remember that an extension functions it just an static util function.
This function below:
fun TextView.setVisibleWithText(text: String){
visibility = View.VISIBLE
setText(text)
}
Will become something like this in java:
class TextViewKt {
public static function setVisibleWithText(#NotNull TextView receiver, #NotNull String text){
receiver.visibility = View.VISIBLE
receiver.setText(text)
}
}
And after you can call it as:
theText?.let { theTextView?.setVisibleWithText(it) }
You can always declare a normal funtion as:
fun setVisibleWithText(textView: TextView, text: String){
textView.visibility = View.VISIBLE
textView.text = text
}
or if you want to make the check inside:
/***
* It makes the textview visible if text is not null (it will stay visible if it was visible before)
**/
fun setVisibleWithText(textView: TextView, text: String?){
text ?: return
textView.visibility = View.VISIBLE
textView.text = text
}
I'm using Anko in my Android project, but I don't know how can it reference the child views I created in the DSL when the referenced view is not at the same level where I reference it.
The following code works:
alert {
customView {
val input = textInputLayout {
editText {
hint = "Name"
textColor =resources.getColor(R.color.highlight)
}
}
positiveButton("OK") { "${input.editText.text}" }
}
}.show()
but the following code does not work:
alert {
customView {
val vertical = verticalLayout {
textView {
text = "Edit device name"
textColor = resources.getColor(R.color.highlight)
textSize = 24F
}
val input = textInputLayout {
editText {
hint = "Name"
textColor = resources.getColor(R.color.highlight)
}
}
}
positiveButton("OK") { "${vertical.input.editText.text}" } // Cannot resolve "input"
}
}.show()
As I see it there are two ways. The super hacky way would be to declare the positive button within the textInputLayout block. This is possible because you can access all outer scopes from within any nested scope and the positiveButton method is declared in the alert scope:
alert {
customView {
verticalLayout {
textInputLayout {
val editText = editText {
hint = "Name"
}
positiveButton("OK") { toast("${editText.text}") }
}
}
}
}.show()
The less hacky way would be to declare a variable that can be accessed from both scopes. However you would need to make it nullable since you can't initialize it immediately:
alert {
var editText: EditText? = null
customView {
verticalLayout {
textInputLayout {
editText = editText {
hint = "Name"
}
}
}
}
positiveButton("OK") { toast("${editText!!.text}") }
}.show()
I propose using findViewById()
alert {
customView {
val vertical = verticalLayout {
textView {
text = "Edit device name"
textSize = 24F
}
val input = textInputLayout {
editText {
id = R.id.my_id_resource // put your id here
hint = "Name"
}
}
}
positiveButton("OK") { "${(vertical.findViewById(R.id.my_id_resource) as? EditText)?.text}" }
}
}.show()
You can always elevate a view, passing the context vertical manually:
customView {
val vertical = verticalLayout {
textView {
text = "Edit device name"
textColor = resources.getColor(R.color.highlight)
textSize = 24F
}
}
val input = /*here:*/ vertical.textInputLayout {
editText {
hint = "Name"
textColor = resources.getColor(R.color.highlight)
}
}
positiveButton("OK") { "${input.editText.text}" }
}
I usually declare a view as a property in a class with lateinit modifier; this way it's not nullable and most of views are declared in one place, improving readability:
lateinit var toolbar: Toolbar
...
appBarLayout {
toolbar = toolbar {}.lparams(width = matchParent, height = matchParent)
}.lparams(width = matchParent)
...
setSupportActionBar(toolbar)
Probably the best way is to use Android IDs for the elements you need to reference later, and the find<T : View>(Int) : T function. This allows you to reference them from anywhere, as long as the view still exists, and you have access to the application/activity scope.
See the Anko Documentation for details
Example case: Dynamically adding buttons to an existing view
verticalLayout {
id = R.id.button_container
}
//note that the code below here may be executed anywhere after the above in your onCreate function
//verticalLayout is a Anko subclass of LinearLayout, so using the android class is valid.
val buttonContainer = find<LinearLayout>(R.id.button_container)
val containerContext = AnkoContext.Companion.create(ctx, buttonContainer)
val button = ctx.button {
text = "click me"
onClick = { toast("created with IDs!") }
}
buttonContainer.addView(button.createView(containerContext, buttonContainer))