I'm trying to achieve a expandable textview which allow user to expand our collapse text longer than specified lines.
However, when the text is expanded, requestLayout of the TextView was called, but LinearLayout instead of expanding itself, it cut the TextView and the other child inside LinearLayout becomes 0dp. (Measured in Layout Insepctor)
The code I use to achieve ExpandableTextView is down below:
ExpandTextView.kt
class ExpandTextView #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : AppCompatTextView(context, attrs, defStyleAttr) {
/**
* true:展开,false:收起
*/
private var mExpanded = false
private var mCallback: ExpandTextViewCallback? = null
/**
* 源文字内容
*/
private var mText = ""
private var maxLineCount = 3
private var ellipsizeText = "..."
fun setMaxLineCount(maxLineCount: Int) {
this.maxLineCount = maxLineCount
}
fun setEllipsizeText(ellipsizeText: String) {
this.ellipsizeText = ellipsizeText
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val staticLayout = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
StaticLayout(mText, paint, measuredWidth - paddingLeft - paddingRight, Layout.Alignment.ALIGN_CENTER, 1f, 0f, true)
} else {
val builder = StaticLayout.Builder.obtain(mText, 0, text.length, paint, measuredWidth - paddingLeft - paddingRight)
.setAlignment(Layout.Alignment.ALIGN_CENTER)
.setLineSpacing(0f, 1f)
.setIncludePad(true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
builder.setUseLineSpacingFromFallbacks(true)
}
builder.build()
}
/**
* 总计行数
*/
var lineCount = staticLayout.lineCount
if (lineCount > maxLineCount) {
when (mExpanded) {
true -> {
mCallback?.onExpand()
text = mText
}
false -> {
mCallback?.onCollapse()
lineCount = maxLineCount
val dotWidth = paint.measureText(ellipsizeText)
val start = staticLayout.getLineStart(lineCount - 1)
val end = staticLayout.getLineEnd(lineCount - 1)
val lineText = mText.substring(start, end)
var endIndex = 0
for (i in lineText.length - 1 downTo 0) {
val str = lineText.substring(i, lineText.length)
if (paint.measureText(str) >= dotWidth) {
endIndex = i
break
}
}
val newEndLineText = lineText.substring(0, endIndex) + ellipsizeText
text = mText.substring(0, start) + newEndLineText
}
}
} else {
//mCallback?.onLoss()
text = mText
}
var lineHeight = 0
for (i in 0 until lineCount) {
val lienBound = Rect()
staticLayout.getLineBounds(i, lienBound)
lineHeight += lienBound.height()
}
lineHeight += paddingTop + paddingBottom
setMeasuredDimension(measuredWidth, lineHeight)
}
fun toggleExpand() {
mExpanded = !mExpanded
requestLayout()
}
fun setText(text: String, callback: ExpandTextViewCallback) {
mText = text
mCallback = callback
setText(text)
}
}
ExpandLayout.kt
class ExpandLayout #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : LinearLayout(context, attrs, defStyleAttr) {
private var binding: LayoutExpandViewBinding
private var maxCollapsedLines = 3
private var contentTextSize = 18f
private var contentTextColor = 0
private var expandText = ""
private var collapseText = ""
private var expandCollapseTextSize = 18f
private var expandCollapseTextColor = 0
private var expandCollapseTextGravity = 0
private var ellipsizeText = "..."
private var middlePadding = 0f
init {
initAttrs(context, attrs, defStyleAttr)
orientation = VERTICAL
binding = LayoutExpandViewBinding.inflate(LayoutInflater.from(context), this, true)
binding.etvContent.setMaxLineCount(maxCollapsedLines)
binding.etvContent.textSize = DensityUtil.px2sp(context, contentTextSize)
binding.etvContent.setTextColor(contentTextColor)
binding.etvContent.setEllipsizeText(ellipsizeText)
val layoutParams = binding.tvTip.layoutParams as LayoutParams
layoutParams.topMargin = middlePadding.toInt()
binding.tvTip.layoutParams = layoutParams
binding.tvTip.textSize = DensityUtil.px2sp(context, expandCollapseTextSize)
binding.tvTip.setTextColor(expandCollapseTextColor)
binding.tvTip.gravity = when (expandCollapseTextGravity) {
0 -> Gravity.LEFT
1 -> Gravity.CENTER
2 -> Gravity.RIGHT
else -> Gravity.LEFT
}
binding.etvContent.requestLayout()
binding.tvTip.requestLayout()
}
private fun initAttrs(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ExpandLayout)
maxCollapsedLines = typedArray.getInt(R.styleable.ExpandLayout_maxCollapsedLines, 3)
contentTextSize = typedArray.getDimension(R.styleable.ExpandLayout_contentTextSize, DensityUtil.sp2px(context, 18f).toFloat())
contentTextColor = typedArray.getColor(R.styleable.ExpandLayout_contentTextColor, resources.getColor(R.color.text_black))
expandText = if (typedArray.getString(R.styleable.ExpandLayout_expandText).isNullOrEmpty()) "全文" else typedArray.getString(R.styleable.ExpandLayout_expandText).toString()
collapseText = if (typedArray.getString(R.styleable.ExpandLayout_collapseText).isNullOrEmpty()) "收起" else typedArray.getString(R.styleable.ExpandLayout_collapseText).toString()
expandCollapseTextSize = typedArray.getDimension(R.styleable.ExpandLayout_expandCollapseTextSize, DensityUtil.sp2px(context, 18f).toFloat())
expandCollapseTextColor = typedArray.getColor(R.styleable.ExpandLayout_expandCollapseTextColor, resources.getColor(R.color.text_blue))
expandCollapseTextGravity = typedArray.getColor(R.styleable.ExpandLayout_expandCollapseTextGravity, 0)
ellipsizeText = if (typedArray.getString(R.styleable.ExpandLayout_ellipsizeText).isNullOrEmpty()) "..." else typedArray.getString(R.styleable.ExpandLayout_ellipsizeText).toString()
middlePadding = typedArray.getDimension(R.styleable.ExpandLayout_middlePadding, 0f)
typedArray.recycle()
}
fun setContent(text: String) {
binding.tvTip.setOnClickListener {
binding.etvContent.toggleExpand()
}
binding.etvContent.setText(text, object : ExpandTextViewCallback {
override fun onExpand() {
binding.tvTip.visibility = View.VISIBLE
binding.tvTip.text = collapseText
}
override fun onCollapse() {
binding.tvTip.visibility = View.VISIBLE
binding.tvTip.text = expandText
}
override fun onLoss() {
binding.tvTip.visibility = View.GONE
}
})
}
}
The ExpandLayout layout xml code:
layout_expand_view.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/ll_expand_view"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.zld.expandlayout.ExpandTextView
android:id="#+id/etv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="#+id/tv_tip"
android:gravity="left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="查看全文"
android:textColor="#39a4d2" />
</LinearLayout>
RecyclerView Item Xml:
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="14dp"
app:cardBackgroundColor="#color/white"
app:cardCornerRadius="8dp"
app:cardElevation="0dp">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Many other views here -->
<com.zld.expandlayout.ExpandLayout
android:id="#+id/message_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
app:contentTextColor="#313131"
app:contentTextSize="12sp"
app:expandText="#string/forum_text_expand"
app:collapseText="#string/forum_text_collapse"
app:maxCollapsedLines="5"
app:expandCollapseTextColor="#color/theme_color"
app:expandCollapseTextGravity="left"
app:expandCollapseTextSize="12sp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
As you can here, when I click the expand or collapse button, the content did expand, but the LinearLayout cut the content, while the expand or collapse button (the other textview) height becomes 0dp.
Related
I'm creating a custom SwipeButton extend RelativeLayout in android which contains an image that I can swipe to right.
On the other hand, I have a Fragment that includes this SwipeButton. My goal is that when swipe has completed (the image has been scrolled to the end) execute a function in fragment
include some code:
1. Fragment
class FirstStep() : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_first_step, container, false)
val swipeButton : SwipeButton = view.findViewById(R.id.swipe_btn)
// I TRIED THIS: ------------------------------
swipeButton.setOnTouchListener { view, motionEvent ->
when(motionEvent.action){
MotionEvent.ACTION_UP -> {
if(swipeButton.completed){
//function that i need to run in firebase
}
return#setOnTouchListener true
}
else -> {return#setOnTouchListener true}
}
}
// I TRIED THIS: ------------------------------
// NOT WORKS *explanation below
return view
}
}
*Didn't work because apparently I think it conflicts with the same on touch listener that is inside the SwipeButton Class and from then the Button not works
2. fragment_first_step.xml
... some layouts
<com.app.SwipeButton
android:id="#+id/swipe_btn"
android:layout_width="wrap_content"
android:layout_weight="1"
android:layout_height="70dp"
/>
3. SwipeButton Class
class SwipeButton : RelativeLayout {
#JvmOverloads
constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : super(context, attrs, defStyleAttr) {
init(context, attrs)
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
constructor(
context: Context,
attrs: AttributeSet?,
defStyleAttr: Int,
defStyleRes: Int
) : super(context, attrs, defStyleAttr, defStyleRes) {
init(context, attrs)
}
private var slidingButton: ImageView? = null
private var initialX : Float = 0f
private var active = false
var completed = false
private var initialButtonWidth : Int = 0
private var centerText: TextView? = null
private var disabledDrawable: Drawable? = null
private var enabledDrawable: Drawable? = null
private fun init(context: Context, attrs: AttributeSet?) {
// add Relative Layout
val background = RelativeLayout(context)
val layoutParamsView = LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
layoutParamsView.addRule(CENTER_IN_PARENT, TRUE)
background.background = ContextCompat.getDrawable(context, R.drawable.shape_rounded)
addView(background, layoutParamsView)
// add text to button
val centerText:TextView = TextView(context)
this.centerText = centerText
val layoutParams = LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
layoutParams.addRule(CENTER_IN_PARENT, TRUE)
centerText.text = "SIGUIENTE PASO" //add any text you need
centerText.setTextColor(Color.WHITE)
centerText.setPadding(35, 35, 35, 35)
background.addView(centerText, layoutParams)
// add image to swipe
val swipeButton = ImageView(context)
slidingButton = swipeButton
disabledDrawable =
ContextCompat.getDrawable(getContext(), R.drawable.ic_home)
enabledDrawable =
ContextCompat.getDrawable(getContext(), R.drawable.ic_close_black)
slidingButton!!.setImageDrawable(disabledDrawable)
slidingButton!!.setPadding(40, 40, 40, 40)
val layoutParamsButton = LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
layoutParamsButton.addRule(ALIGN_PARENT_LEFT, TRUE)
layoutParamsButton.addRule(CENTER_VERTICAL, TRUE)
swipeButton.background = ContextCompat.getDrawable(context, R.drawable.shape_button)
swipeButton.setImageDrawable(disabledDrawable)
addView(swipeButton, layoutParamsButton)
// LISTEN TO TOUCH CHANGES
setOnTouchListener(getButtonTouchListener())
this.onFinishInflate()
}
#SuppressLint("ClickableViewAccessibility")
private fun getButtonTouchListener(): OnTouchListener? {
return OnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> return#OnTouchListener true
MotionEvent.ACTION_MOVE -> {
if (initialX.toInt() == 0) {
initialX = slidingButton!!.x;
}
if ((event.x > initialX + slidingButton!!.width.toFloat() / 2) && (event.x + slidingButton!!.width / 2 < width)) {
slidingButton!!.setX(event.x - slidingButton!!.width / 2);
centerText?.setAlpha(1 - 1.3f * (slidingButton!!.x + slidingButton!!.width) / width);
}
if (event.x + slidingButton!!.width / 2 > width && slidingButton!!.x + slidingButton!!.width / 2 < width) {
slidingButton!!.setX((width - slidingButton!!.width).toFloat());
}
if (event.x < slidingButton!!.width / 2 && slidingButton!!.x > 0) {
slidingButton!!.setX(0F)
}
}
MotionEvent.ACTION_UP -> {
if (active) {
collapseButton();
} else {
initialButtonWidth = slidingButton!!.width
if (slidingButton!!.x + slidingButton!!.width > width * 0.85) {
expandButton();
completed = true
// HERE SWIPE IS COMPLETED
} else {
moveButtonBack();
}
}
}
}
false
}
}
private fun expandButton() {
val positionAnimator = ValueAnimator.ofFloat(slidingButton!!.x, 0f)
positionAnimator.addUpdateListener {
val x = positionAnimator.animatedValue as Float
slidingButton!!.x = x
}
val widthAnimator = ValueAnimator.ofInt(
slidingButton!!.width,
width
)
widthAnimator.addUpdateListener {
val params = slidingButton!!.layoutParams
params.width = (widthAnimator.animatedValue as Int)
slidingButton!!.layoutParams = params
}
val animatorSet = AnimatorSet()
animatorSet.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
active = true
slidingButton!!.setImageDrawable(enabledDrawable)
}
})
animatorSet.playTogether(positionAnimator, widthAnimator)
animatorSet.start()
}
private fun collapseButton() {
val widthAnimator = ValueAnimator.ofInt(
slidingButton!!.width,
initialButtonWidth
)
widthAnimator.addUpdateListener {
val params = slidingButton!!.layoutParams
params.width = (widthAnimator.animatedValue as Int)
slidingButton!!.layoutParams = params
}
widthAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
active = false
slidingButton!!.setImageDrawable(disabledDrawable)
}
})
val objectAnimator = ObjectAnimator.ofFloat(
centerText!!, "alpha", 1f
)
val animatorSet = AnimatorSet()
animatorSet.playTogether(objectAnimator, widthAnimator)
animatorSet.start()
}
private fun moveButtonBack() {
val positionAnimator = ValueAnimator.ofFloat(slidingButton!!.x, 0f)
positionAnimator.interpolator = AccelerateDecelerateInterpolator()
positionAnimator.addUpdateListener {
val x = positionAnimator.animatedValue as Float
slidingButton!!.x = x
}
val objectAnimator = ObjectAnimator.ofFloat(
centerText!!, "alpha", 1f
)
positionAnimator.duration = 200
val animatorSet = AnimatorSet()
animatorSet.playTogether(objectAnimator, positionAnimator)
animatorSet.start()
}
}
any help would be greatly appreciated
Thanks!!
I have an example android app in Kotlin. On my activity I have images displayed in gridview, and I want them randomly to disapear (or change with some other image), but I can't figure out where to write that code, because onStart or onResume did not worked for me.
Here is my code:
class GameActivity : AppCompatActivity()
{
private var settings: Settings = Settings(1, 2, 2, 2)
private var _settingsRepository: SettingsRepository? = null;
private var _handler: Handler = Handler()
private var _runnable: Runnable = Runnable { }
private var counter: Int = 0
private var _adapter: GridElementAdapter? = null;
private var _gridElementList: List<Item> = ArrayList<Item>()
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_game)
_settingsRepository = SettingsRepository(this.applicationContext)
settings = _settingsRepository!!.read()
gridView.numColumns = settings.column
gridView.setExpanded(true)
val windowSize: Size = getGridSize()
_gridElementList = getData()
_adapter = GridElementAdapter(this, _gridElementList, settings, windowSize)
gridView.adapter = _adapter;
}
override fun onStart()
{
super.onStart()
hideElements()
startCounter()
}
private fun hideElements()
{
if(gridView.adapter.count == 0)
{
return
}
var hideOrdder: ArrayList<Int> = ArrayList<Int>()
for (i in 0 until gridView.adapter.count)
{
hideOrdder.add(i)
}
hideOrdder.shuffle()
for (i in hideOrdder)
{
try
{
var gridChild: Item = gridView.adapter.getItem(i) as Item
val element: Drawable? = getDrawable(gridChild.drawableId)
Thread.sleep((settings!!.speed * 100).toLong())
}
catch (e:Exception)
{
print(e.localizedMessage)
}
}
}
private fun getData() : List<Item>
{
var gridElements: ArrayList<Item> = ArrayList<Item>()
gridElements.add(Item("11", R.drawable.makk_asz))
gridElements.add(Item("12", R.drawable.makk_kiraly))
gridElements.add(Item("13", R.drawable.makk_felso))
gridElements.add(Item("14", R.drawable.makk_also))
gridElements.add(Item("15", R.drawable.makk_tizes))
gridElements.add(Item("16", R.drawable.makk_kilences))
gridElements.add(Item("17", R.drawable.makk_nyolcas))
gridElements.add(Item("18", R.drawable.makk_hetes))
gridElements.add(Item("21", R.drawable.piros_asz))
gridElements.add(Item("22", R.drawable.piros_kiraly))
gridElements.add(Item("23", R.drawable.piros_felso))
gridElements.add(Item("24", R.drawable.piros_also))
gridElements.add(Item("25", R.drawable.piros_tizes))
gridElements.add(Item("26", R.drawable.piros_kilences))
gridElements.add(Item("27", R.drawable.piros_nyolcas))
gridElements.add(Item("28", R.drawable.piros_hetes))
gridElements.add(Item("31", R.drawable.zold_asz))
gridElements.add(Item("31", R.drawable.zold_kiraly))
gridElements.add(Item("33", R.drawable.zold_felso))
gridElements.add(Item("34", R.drawable.zold_also))
gridElements.add(Item("35", R.drawable.zold_tizes))
gridElements.add(Item("36", R.drawable.zold_kilences))
gridElements.add(Item("37", R.drawable.zold_nyolcas))
gridElements.add(Item("38", R.drawable.zold_hetes))
gridElements.add(Item("41", R.drawable.tok_asz))
gridElements.add(Item("42", R.drawable.tok_kiraly))
gridElements.add(Item("43", R.drawable.tok_felso))
gridElements.add(Item("44", R.drawable.tok_also))
gridElements.add(Item("45", R.drawable.tok_tizes))
gridElements.add(Item("46", R.drawable.tok_kilences))
gridElements.add(Item("47", R.drawable.tok_nyolcas))
gridElements.add(Item("48", R.drawable.tok_hetes))
val count: Int = settings.column * settings.row
gridElements.shuffle()
return gridElements.take(count)
}
private fun startCounter()
{
_runnable = object : Runnable
{
override fun run()
{
counter++
_handler.postDelayed(this, 1000)
}
}
_handler.post(_runnable)
}
private fun stopCounter()
{
_handler.removeCallbacks(_runnable)
counter = 0
}
private fun getGridSize() : Size
{
val displaymetrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(displaymetrics);
val screenHeight = displaymetrics.heightPixels
val screenWidth = displaymetrics.widthPixels
val height: Int = ( (screenHeight) / settings!!.row * 0.8).toInt()
val width: Int = ((screenWidth / settings!!.column) * 0.62).toInt()
return Size(width, height)
}
}
class GridElementAdapter: BaseAdapter
{
private var _gridElementList: List<Item> = ArrayList<Item>()
private var _context: Activity? = null
private var _settings: Settings? = null
private var _dimension: Size? = null
private var _loyoutInflater: LayoutInflater? = null
constructor(context: Activity, list: List<Item>, settings: Settings, dimension: Size) : super()
{
_context = context
_gridElementList = list
_settings = settings
_dimension = dimension
_loyoutInflater = LayoutInflater.from(context)
}
override fun getCount(): Int {
return _gridElementList.size
}
override fun getItem(i: Int): Item
{
return _gridElementList[i]
}
override fun getItemId(i: Int): Long
{
return _gridElementList[i].drawableId.toLong()
}
override fun getView(i: Int, view: View?, viewGroup: ViewGroup?): View?
{
var v = view
val picture: ImageView
val name: TextView
if (v == null)
{
v = _loyoutInflater!!.inflate(R.layout.grid_item, viewGroup, false)
v.setTag(R.id.picture, v.findViewById(R.id.picture))
v.setTag(R.id.text, v.findViewById(R.id.text))
v.setId(R.id.text.toInt())
}
picture = v!!.getTag(R.id.picture) as ImageView
name = v!!.getTag(R.id.text) as TextView
val item = getItem(i)
picture.setImageResource(item.drawableId)
name.text = item.name
return v
}
}
class Item
{
var name: String
var drawableId: Int = 0
constructor(name: String, drawableId: Int)
{
this.name = name
this.drawableId = drawableId
}
}
activity_game.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".GameActivity">
<com.memorygame.components.GameGridView
android:id="#+id/gridView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center|center_horizontal|fill_horizontal|center_vertical|fill_vertical"
android:horizontalSpacing="2dp"
android:isScrollContainer="false"
android:stretchMode="columnWidth"
android:verticalSpacing="5dp" />
<LinearLayout
android:id="#+id/counterLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_column="2"
android:gravity="bottom"
android:orientation="horizontal"
android:visibility="visible">
<TextView
android:id="#+id/timeLabelTextView"
android:layout_width="301dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="Eltelt idő"
android:textAlignment="center"
android:textSize="24sp" />
<TextView
android:id="#+id/timeTextView"
android:layout_width="168dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="0"
android:textAlignment="center"
android:textSize="24sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
grid_item.xml
<?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"
android:layout_gravity="center_horizontal|fill_horizontal|center_vertical|fill_vertical">
<com.memorygame.components.SquareImageView
android:id="#+id/picture"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY" />
<TextView
android:id="#+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="#55000000"
android:paddingLeft="10dp"
android:paddingTop="15dp"
android:paddingRight="10dp"
android:paddingBottom="15dp"
android:textColor="#android:color/white" />
</FrameLayout>
class GameGridView: GridView
{
private var expanded: Boolean = false
constructor(context: Context?): super(context)
{}
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
{}
constructor(context: Context?, attrs: AttributeSet?, defStyle: Int): super(context, attrs, defStyle)
{}
private fun isExpanded(): Boolean
{
return expanded
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int)
{
// HACK! TAKE THAT ANDROID!
if (isExpanded())
{
// Calculate entire height by providing a very large height hint.
// View.MEASURED_SIZE_MASK represents the largest height possible.
val expandSpec = MeasureSpec.makeMeasureSpec(MEASURED_SIZE_MASK, AT_MOST)
super.onMeasure(widthMeasureSpec, expandSpec)
val params: ViewGroup.LayoutParams = layoutParams
params.height = measuredHeight
}
else
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
}
fun setExpanded(expanded: Boolean)
{
this.expanded = expanded
}
}
class SquareImageView : androidx.appcompat.widget.AppCompatImageView
{
constructor(context: Context) : super(context) {}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {}
constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) {}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
setMeasuredDimension(measuredWidth, measuredWidth)
}
}
My problem is that I don't know how to hide the images in the grid, after the UI is initialized, and what would be the best practice.
I recommend to use RecyclerView with GridLayoutManager.
Then you can update items by adapter.submitList(items).
I have a custom EditText providig PinEntry functionality looking like this:
class PinEntryView : EditText {
private var mSpace = 15f
private var mCharSize = 0f
private var mNumChars = 4f
private var mLineSpacing = 8f
private val XML_NAMESPACE_ANDROID = "http://schemas.android.com/apk/res/android"
private var mClickListener: View.OnClickListener? = null
private var mLineStroke = 1f
private var mLinesPaint: Paint? = null
private var mOnPinEnteredListener: OnPinEnteredListener? = null
private fun updateColorForLines(next: Boolean) {
if (isFocused) {
mLinesPaint!!.color = getColor(context, android.R.color.transparent)
if (next) {
mLinesPaint!!.color = getColor(context, R.color.edit_text_background)
}
} else {
mLinesPaint!!.color = getColor(context, android.R.color.transparent)
}
}
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(context, attrs)
}
constructor(
context: Context, attrs: AttributeSet,
defStyleAttr: Int
) : super(context, attrs, defStyleAttr) {
init(context, attrs)
}
private fun init(context: Context, attrs: AttributeSet) {
val multi = context.resources.displayMetrics.density
mLineStroke *= multi
mLinesPaint = Paint(paint)
mLinesPaint!!.strokeWidth = mLineStroke
mSpace *= multi
mLineSpacing *= multi
val mMaxLength = attrs.getAttributeIntValue(
XML_NAMESPACE_ANDROID,
"maxLength",
6
)
paint.color = getColor(context, android.R.color.white)
mNumChars = mMaxLength.toFloat()
super.setCustomSelectionActionModeCallback(
object : ActionMode.Callback {
override fun onPrepareActionMode(
mode: ActionMode,
menu: Menu
): Boolean {
return false
}
override fun onDestroyActionMode(mode: ActionMode) {}
override fun onCreateActionMode(
mode: ActionMode,
menu: Menu
): Boolean {
return false
}
override fun onActionItemClicked(
mode: ActionMode,
item: MenuItem
): Boolean {
return false
}
})
super.setOnClickListener { v ->
setSelection(text.length)
if (mClickListener != null) {
mClickListener!!.onClick(v)
}
}
}
override fun setOnClickListener(l: OnClickListener) {
mClickListener = l
}
override fun onDraw(canvas: Canvas) {
val availableWidth = width - paddingRight - paddingLeft
mCharSize = if (mSpace < 0) {
(availableWidth / (mNumChars * 2 - 1))
} else {
(availableWidth - mSpace * (mNumChars - 1)) / mNumChars
}
var startX = paddingLeft.toFloat()
val bottom = height.toFloat() - paddingBottom.toFloat()
val text = text
val textLength = text.length
val textWidths = FloatArray(textLength)
paint.getTextWidths(getText(), 0, textLength, textWidths)
for (i in 0 until mNumChars.toInt()) {
updateColorForLines(i == textLength)
canvas.drawRoundRect(
startX, 0f, startX + mCharSize, height.toFloat(), 5f, 5f, mLinesPaint
)
if (text.length > i) {
val middle = startX + mCharSize / 2
canvas.drawText(
"******",
i,
i + 1,
middle - textWidths[0] / 2,
bottom - mLineSpacing,
paint
)
}
startX += if (mSpace < 0) {
mCharSize * 2
} else {
mCharSize + mSpace
}
}
}
override fun onTextChanged(text: CharSequence, start: Int, lengthBefore: Int, lengthAfter: Int) {
if (mOnPinEnteredListener != null && text.length.toFloat() == mNumChars) {
mOnPinEnteredListener!!.onPinEntered(text)
}
}
fun setOnPinEnteredListener(l: OnPinEnteredListener) {
mOnPinEnteredListener = l
}
interface OnPinEnteredListener {
fun onPinEntered(str: CharSequence)
}
}
I followed this tutorial: https://medium.com/#ali.muzaffar/building-a-pinentryedittext-in-android-5f2eddcae5d3
And I am getting behavior showed on picture:
But I would like to have a behavior as shown here:
How can I achieve that? How can I add those default dots/symbols? I tried adding it as a hint or default text, but it does not work because of my onDraw function
Hopefully you found already your solution. But here is how I would've done it:
The PinEntryEditText has an attribute you can set in the .xml file.
Adding this to the element will do the trick: app:pinSingleCharHint="•"
For more customisation check out the sample xml from this page
Background
I'm try to achieve something similar to what the Camera app has for its modes:
I might probably not need to have a ViewPager, as seems that it uses above the horizontal list, but could be nice to have it as an option.
The problem
While technically I succeeded to have the RecyclerView center its items, it doesn't let you actually have the all items to be able to be on the center (example is the first/last one). When you try to scroll to the first or last items, it doesn't let you , because you've reached the edge of the RecyclerView :
Not only that, but in the beginning it doesn't really center, and if I have the RecyclerView have few items, it becomes a problem because I want them to be centered, but having android:layout_width="match_parent" (because all of it should be touch-able) produces this:
while having android:layout_width="wrap_content" get me this:
On both cases, I can't scroll at all. When it's "wrap_content", it's a problem because I won't be able to scroll on the sides.
What I've tried
It's possible to snap the items so that there will always be an item in the center of RecyclerView as such :
val snapHelper = LinearSnapHelper()
snapHelper.attachToRecyclerView(categoriesRecyclerView)
We can also get which is the item in the center (as shown here), by having a scroll listener and using snapHelper.findSnapView(layoutManagaer).
But as I wrote, I can't really have the first/last item being selected this way, because I can't scroll to it so that it will be at the middle.
I tried to look at the docs of the related classes, but I can't find such a thing.
Here's the current code (sample available here) :
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val holder = object : RecyclerView.ViewHolder(
LayoutInflater.from(this#MainActivity).inflate(
R.layout.list_item,
parent,
false
)
) {}
holder.itemView.setOnClickListener {
}
return holder
}
override fun getItemCount(): Int = 20
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
holder.itemView.textView.text = "pos:$position"
}
}
val snapHelper = LinearSnapHelper()
snapHelper.attachToRecyclerView(recyclerView)
}
}
activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout
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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView android:background="#66000000"
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="#dimen/list_item_size"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="#layout/list_item"/>
</androidx.constraintlayout.widget.ConstraintLayout>
list_item.xml
<TextView
android:id="#+id/textView" 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"
android:layout_width="wrap_content" android:layout_height="#dimen/list_item_size"
android:background="?attr/selectableItemBackground" android:breakStrategy="balanced" android:clickable="true"
android:focusable="true" android:gravity="center" android:maxLines="1" android:padding="8dp"
android:shadowColor="#222" android:shadowDx="1" android:shadowDy="1" android:textColor="#fff"
app:autoSizeTextType="uniform" tools:targetApi="m" tools:text="#tools:sample/lorem"/>
The question
How can I let the user freely scroll inside, so that the edge will be determined by whether the first/last item is in the middle? How can I always have the items centered, including when I just started seeing the RecyclerView), and including when there are few of them?
I gave this a try
5 items: https://drive.google.com/open?id=1RPyiY9UndXcrbfBDWLB-UklxjPKMiR8-
2 items: https://drive.google.com/open?id=1HkG8NShxQ3illFupK-urSPwsUhag74WS
First, apply an item decoration to center the first and last items:
class CenterDecoration(#Px private val spacing: Int) : RecyclerView.ItemDecoration() {
private var firstViewWidth = -1
private var lastViewWidth = -1
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
val adapterPosition = (view.layoutParams as RecyclerView.LayoutParams).viewAdapterPosition
val lm = parent.layoutManager as LinearLayoutManager
if (adapterPosition == 0) {
// Invalidate decorations when this view width has changed
if (view.width != firstViewWidth) {
view.doOnPreDraw { parent.invalidateItemDecorations() }
}
firstViewWidth = view.width
outRect.left = parent.width / 2 - view.width / 2
// If we have more items, use the spacing provided
if (lm.itemCount > 1) {
outRect.right = spacing / 2
} else {
// Otherwise, make sure this to fill the whole width with the decoration
outRect.right = outRect.left
}
} else if (adapterPosition == lm.itemCount - 1) {
// Invalidate decorations when this view width has changed
if (view.width != lastViewWidth) {
view.doOnPreDraw { parent.invalidateItemDecorations() }
}
lastViewWidth = view.width
outRect.right = parent.width / 2 - view.width / 2
outRect.left = spacing / 2
} else {
outRect.left = spacing / 2
outRect.right = spacing / 2
}
}
}
Now, LinearSnapHelper determines the center of a view and includes its decorations. You can create a custom one that excludes the decorations from the calculation to center the view only:
/**
* A LinearSnapHelper that ignores item decorations to determine a view's center
*/
class CenterSnapHelper : LinearSnapHelper() {
private var verticalHelper: OrientationHelper? = null
private var horizontalHelper: OrientationHelper? = null
private var scrolled = false
private var recyclerView: RecyclerView? = null
private val scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == RecyclerView.SCROLL_STATE_IDLE && scrolled) {
if (recyclerView.layoutManager != null) {
val view = findSnapView(recyclerView.layoutManager)
if (view != null) {
val out = calculateDistanceToFinalSnap(recyclerView.layoutManager!!, view)
if (out != null) {
recyclerView.smoothScrollBy(out[0], out[1])
}
}
}
scrolled = false
} else {
scrolled = true
}
}
}
fun scrollTo(position: Int, smooth: Boolean) {
if (recyclerView?.layoutManager != null) {
val viewHolder = recyclerView!!.findViewHolderForAdapterPosition(position)
if (viewHolder != null) {
val distances = calculateDistanceToFinalSnap(recyclerView!!.layoutManager!!, viewHolder.itemView)
if (smooth) {
recyclerView!!.smoothScrollBy(distances!![0], distances[1])
} else {
recyclerView!!.scrollBy(distances!![0], distances[1])
}
} else {
if (smooth) {
recyclerView!!.smoothScrollToPosition(position)
} else {
recyclerView!!.scrollToPosition(position)
}
}
}
}
override fun findSnapView(layoutManager: RecyclerView.LayoutManager?): View? {
if (layoutManager == null) {
return null
}
if (layoutManager.canScrollVertically()) {
return findCenterView(layoutManager, getVerticalHelper(layoutManager))
} else if (layoutManager.canScrollHorizontally()) {
return findCenterView(layoutManager, getHorizontalHelper(layoutManager))
}
return null
}
override fun attachToRecyclerView(recyclerView: RecyclerView?) {
this.recyclerView = recyclerView
recyclerView?.addOnScrollListener(scrollListener)
}
override fun calculateDistanceToFinalSnap(
layoutManager: RecyclerView.LayoutManager,
targetView: View
): IntArray? {
val out = IntArray(2)
if (layoutManager.canScrollHorizontally()) {
out[0] = distanceToCenter(layoutManager, targetView, getHorizontalHelper(layoutManager))
} else {
out[0] = 0
}
if (layoutManager.canScrollVertically()) {
out[1] = distanceToCenter(layoutManager, targetView, getVerticalHelper(layoutManager))
} else {
out[1] = 0
}
return out
}
private fun findCenterView(
layoutManager: RecyclerView.LayoutManager,
helper: OrientationHelper
): View? {
val childCount = layoutManager.childCount
if (childCount == 0) {
return null
}
var closestChild: View? = null
val center: Int = if (layoutManager.clipToPadding) {
helper.startAfterPadding + helper.totalSpace / 2
} else {
helper.end / 2
}
var absClosest = Integer.MAX_VALUE
for (i in 0 until childCount) {
val child = layoutManager.getChildAt(i)
val childCenter = if (helper == horizontalHelper) {
(child!!.x + child.width / 2).toInt()
} else {
(child!!.y + child.height / 2).toInt()
}
val absDistance = Math.abs(childCenter - center)
if (absDistance < absClosest) {
absClosest = absDistance
closestChild = child
}
}
return closestChild
}
private fun distanceToCenter(
layoutManager: RecyclerView.LayoutManager,
targetView: View,
helper: OrientationHelper
): Int {
val childCenter = if (helper == horizontalHelper) {
(targetView.x + targetView.width / 2).toInt()
} else {
(targetView.y + targetView.height / 2).toInt()
}
val containerCenter = if (layoutManager.clipToPadding) {
helper.startAfterPadding + helper.totalSpace / 2
} else {
helper.end / 2
}
return childCenter - containerCenter
}
private fun getVerticalHelper(layoutManager: RecyclerView.LayoutManager): OrientationHelper {
if (verticalHelper == null || verticalHelper!!.layoutManager !== layoutManager) {
verticalHelper = OrientationHelper.createVerticalHelper(layoutManager)
}
return verticalHelper!!
}
private fun getHorizontalHelper(
layoutManager: RecyclerView.LayoutManager
): OrientationHelper {
if (horizontalHelper == null || horizontalHelper!!.layoutManager !== layoutManager) {
horizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager)
}
return horizontalHelper!!
}
}
Usage:
class MainActivity : AppCompatActivity() {
private val snapHelper = CenterSnapHelper()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recyclerView.addItemDecoration(CenterDecoration(0))
snapHelper.attachToRecyclerView(recyclerView)
recyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val holder = object : RecyclerView.ViewHolder(
LayoutInflater.from(this#MainActivity).inflate(
R.layout.list_item,
parent,
false
)
) {}
holder.itemView.setOnClickListener {
if (holder.adapterPosition != RecyclerView.NO_POSITION) {
snapHelper.scrollTo(holder.adapterPosition, true)
}
}
return holder
}
override fun getItemCount(): Int = 20
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
holder.itemView.textView.text = "pos:$position"
}
}
}
}
Posting XML here in case someone wants to check this out:
MainActivity
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:layout_width="4dp"
android:layout_height="0dp"
android:background="#color/colorAccent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
list_item.xml
<TextView
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" android:id="#+id/textView"
android:layout_width="wrap_content" android:layout_height="#dimen/list_item_size"
android:background="?attr/selectableItemBackground" android:clickable="true"
android:focusable="true" android:gravity="center" android:maxLines="1" android:padding="8dp"
android:shadowColor="#222" android:shadowDx="1" android:shadowDy="1" android:textColor="#fff"
tools:targetApi="m" tools:text="#tools:sample/lorem"/>
EDIT: here's a sample of how to use this:
http://s000.tinyupload.com/?file_id=01184747175525079378
This is how i solved it. The problem on custom snaphelper and decorators is that they dont work with other libaries and custom Views. It also works with items with variable widths.
If you want to snap the items, just use the classic snaphelper on the recyclerview
public class CenterRecyclerView extends RecyclerView {
public CenterRecyclerView(#NonNull Context context) {
super(context);
}
public CenterRecyclerView(#NonNull Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
}
public CenterRecyclerView(#NonNull Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void updatePadding() {
post(() -> {
final DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics();
final int screenWidth = displayMetrics.widthPixels;
final int screenHeight = displayMetrics.heightPixels;
ViewHolder firstViewHolder = findViewHolderForAdapterPosition(0);
if (firstViewHolder != null) {
firstViewHolder.itemView.measure(WRAP_CONTENT, WRAP_CONTENT);
int viewWidth = firstViewHolder.itemView.getMeasuredWidth();
int padding;
if (screenHeight > screenWidth) {
//Portrait
padding = screenWidth / 2 - viewWidth / 2;
} else {
//Landscape
padding = screenHeight / 2 - viewWidth / 2;
}
setPadding(padding, 0, padding, 0);
} else {
Log.e("CenterRecyclerView", "Could not get first ViewHolder");
}
});
}
#Override
protected void onFinishInflate() {
super.onFinishInflate();
updatePadding();
}
#Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
updatePadding();
}
}
I'm building an MultiStateToggle Button.
To highlight the selected state, I need to draw a rectangle which is exactly the size of a button. This rectangle slides to the current state. Therefore I need to draw, but whatever I do, I do not see any change even though onDraw was called.
The superclass ToggleButton simply holds some constants. It extends LinearLayout!
Does anybody know, why drawing does not work in this LinearLayout sub-class?
class MultiStateToggleButton : ToggleButton {
//region Variables
private val mHighlightPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
#DrawableRes
private val iSelectableItemBackgroundDrawableResId: Int
private val mButtons = mutableListOf<Button>()
//endregion
constructor(context: Context) : this(context, Any() as AttributeSet)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
val mTypedValue = TypedValue()
context.theme.resolveAttribute(R.attr.colorPrimary, mTypedValue, true)
val iPrimaryColor = mTypedValue.data
context.theme.resolveAttribute(R.attr.colorPrimary, mTypedValue, true)
val iPrimaryDarkColor = mTypedValue.data
setBackgroundColor(iPrimaryDarkColor)
context.theme.resolveAttribute(R.attr.colorAccent, mTypedValue, true)
val iAccentColor = mTypedValue.data
val mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.MultiStateToggleButton)
foregroundColor = mTypedArray.getColor(R.styleable.MultiStateToggleButton_foregroundColor, iPrimaryColor)
highlightColor = mTypedArray.getColor(R.styleable.MultiStateToggleButton_highlightColor, iAccentColor)
val mTexts = mTypedArray.getTextArray(R.styleable.MultiStateToggleButton_elements)
if (mTexts != null) {
val iSelectedElement = mTypedArray.getInt(R.styleable.MultiStateToggleButton_selectedElement, 0)
setElements(Array(mTexts.size) { i ->
mTexts[i].toString()
}, iSelectedElement)
}
mTypedArray.recycle()
}
init {
val mTypedValue = TypedValue()
context.theme.resolveAttribute(android.R.attr.selectableItemBackground, mTypedValue, true)
iSelectableItemBackgroundDrawableResId = mTypedValue.resourceId
mHighlightPaint.apply {
color = highlightColor
style = Paint.Style.FILL
}
}
//region Public Variables
override var selectedElement: Int = 0
set(value) {
selectButton(value)
field = value
super.selectedElement = value
}
//endregion
//region Methods
fun setElements(texts: Array<String>) {
removeAllViews()
mButtons.clear()
texts.forEachIndexed { i, text ->
val mButton = Button(context).apply {
layoutParams = LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, LAYOUT_WEIGHT_EQUAL)
setBackgroundResource(iSelectableItemBackgroundDrawableResId)
setText(text)
setTextColor(textColor)
setOnClickListener { selectedElement = i }
}
mButtons.add(mButton)
addView(mButton)
}
}
fun setElements(texts: Array<String>, selectedItem: Int) {
setElements(texts)
selectButton(selectedItem)
}
override fun setEnabled(enabled: Boolean) {
for (i in 0 until childCount) {
getChildAt(i).isEnabled = enabled
}
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.drawRect(0f, 0f, 30f, 30f, mHighlightPaint)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
super.onLayout(changed, l, t, r, b)
setWillNotDraw(false)
}
override fun onSaveInstanceState(): Parcelable {
return Bundle().apply {
putParcelable(KEY_INSTANCE_STATE, super.onSaveInstanceState())
putInt(KEY_SELECTED_BUTTON, selectedElement)
}
}
override fun onRestoreInstanceState(state: Parcelable?) {
var mRestoredState: Parcelable? = state
if (state is Bundle) {
selectButton(state.getInt(KEY_SELECTED_BUTTON))
mRestoredState = state.getParcelable(KEY_INSTANCE_STATE)
}
super.onRestoreInstanceState(mRestoredState)
}
private fun selectButton(position: Int) {
if (mButtons.isNotEmpty()) {
if (position >= mButtons.size) {
throw IndexOutOfBoundsException("Position was $position but there are only ${mButtons.size} Buttons.")
}
getChildAt(selectedElement).isEnabled = true
getChildAt(position).isEnabled = false
invalidate()
}
}
//endregion
companion object {
private const val KEY_SELECTED_BUTTON = "mtb_selected_button"
private const val KEY_INSTANCE_STATE = "mtb_instance_state"
}
}
As Kotlins' init block is called on Primary Constructor call, my Paint was not initialized.