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
Related
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.
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 horizontal recyclerViews in vertical RecyclerView. To scroll horizontally I should swipe from 45 degrees to -45 degree. To scroll vertically from 90 degrees to 45 degrees. Is any possibility to override this?
For example, vertical scroll works only with the swipe from 105 to 75 degrees. Else, horizontal scroll.
You need to create MainVerticalRecyclerView like this:
open class MainVerticalRecyclerView : RecyclerView {
private var scrollPointerId = -1
private var pointTouchX = 0
private var pointTouchY = 0
private var touchSlopType = 0
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
val vc = ViewConfiguration.get(context)
touchSlopType = vc.scaledTouchSlop
}
override fun setScrollingTouchSlop(slopConstant: Int) {
super.setScrollingTouchSlop(slopConstant)
val vc = ViewConfiguration.get(context)
when (slopConstant) {
TOUCH_SLOP_DEFAULT -> touchSlopType = vc.scaledTouchSlop
TOUCH_SLOP_PAGING -> touchSlopType = vc.scaledPagingTouchSlop
}
}
override fun onInterceptTouchEvent(e: MotionEvent?): Boolean {
if (e == null) {
return false
}
val action = e.actionMasked
val actionIndex = e.actionIndex
when (action) {
MotionEvent.ACTION_DOWN -> {
scrollPointerId = e.getPointerId(0)
pointTouchX = Math.round(e.x + 0.5f)
pointTouchY = Math.round(e.y + 0.5f)
return super.onInterceptTouchEvent(e)
}
MotionEvent.ACTION_POINTER_DOWN -> {
scrollPointerId = e.getPointerId(actionIndex)
pointTouchX = Math.round(e.getX(actionIndex) + 0.5f)
pointTouchY = Math.round(e.getY(actionIndex) + 0.5f)
return super.onInterceptTouchEvent(e)
}
MotionEvent.ACTION_MOVE -> {
val index = e.findPointerIndex(scrollPointerId)
if (index < 0) {
return false
}
val x = Math.round(e.getX(index) + 0.5f)
val y = Math.round(e.getY(index) + 0.5f)
if (scrollState != SCROLL_STATE_DRAGGING) {
val dx = x - pointTouchX
val dy = y - pointTouchY
var startScroll = false
if (layoutManager?.canScrollHorizontally() == true && Math.abs(dx) > touchSlopType && (layoutManager?.canScrollVertically() == true || Math.abs(dx) > Math.abs(dy))) {
startScroll = true
}
if (layoutManager?.canScrollVertically() == true && Math.abs(dy) > touchSlopType && (layoutManager?.canScrollHorizontally() == true || Math.abs(dy) > Math.abs(dx))) {
startScroll = true
}
return startScroll && super.onInterceptTouchEvent(e)
}
return super.onInterceptTouchEvent(e)
}
else -> {
return super.onInterceptTouchEvent(e)
}
}
}
}
and create ChildHorizontalRecyclerView for other nested RecyclerView inside main RecyclerView like this:
class ChildHorizontalRecyclerView : MainVerticalRecyclerView {
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {}
}
if you want scroll like in PlayStore when you touch HorizontalChildRecyclerView need write:
recyclerView.addOnItemTouchListener(object : RecyclerView.OnItemTouchListener {
override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) {}
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
if (e.action == MotionEvent.ACTION_DOWN && rv.scrollState == RecyclerView.SCROLL_STATE_SETTLING) {
rv.stopScroll()
}
return false
}
override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {}
})
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.
I have this custom ViewPager created from here (Android: Vertical ViewPager):
class VerticalViewPager : ViewPager {
#JvmOverloads
constructor(
context: Context,
attrs: AttributeSet? = null)
: super(context, attrs){
setPageTransformer(false, DefaultTransformer())
overScrollMode = OVER_SCROLL_NEVER
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
val intercepted = super.onInterceptTouchEvent(swapXY(ev))
swapXY(ev) // return touch coordinates to original reference frame for any child views
return intercepted
}
override fun onTouchEvent(ev: MotionEvent?): Boolean {
return super.onTouchEvent(ev)
}
/**
* Swaps the X and Y coordinates of your touch event.
*/
private fun swapXY(ev: MotionEvent): MotionEvent {
val width = width.toFloat()
val height = height.toFloat()
val newX = ev.y / height * width
val newY = ev.x / width * height
ev.setLocation(newX, newY)
return ev
}
private class VerticalPageTransformer : ViewPager.PageTransformer{
override fun transformPage(view: View, position: Float) {
if (position < -1) { // [-Infinity,-1)
// This page is way off-screen to the left.
view.alpha = 0f
} else if (position <= 1) { // [-1,1]
view.alpha = 1f
// Counteract the default slide transition
view.translationX = view.getWidth() * -position
//set Y position to swipe in from top
val yPosition = position * view.getHeight()
view.translationY = yPosition
} else { // (1,+Infinity]
// This page is way off-screen to the right.
view.alpha = 0f
}
}
}
}
I use an adapter that loads an ImageView inside each page. This ImageView has a onClickListener.
open class VerticalViewPagerAdapter(var mContext: Context, val photos : RealmList<ProfilePhoto>?, onPhotoClick: OnPhotoClick?) : PagerAdapter() {
var mLayoutInflater : LayoutInflater? = null
val onPhotoListener = onPhotoClick
init {
mLayoutInflater = mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
}
override fun getCount(): Int {
return photos?.size!!
}
override fun isViewFromObject(view: View, `object`: Any): Boolean {
return view === `object` as FrameLayout
}
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val itemView = mLayoutInflater?.inflate(R.layout.view_image_profile_viewpager_item, container, false)
val imageView = itemView?.findViewById(R.id.profileImageItemIv) as ImageView
imageView.setOnClickListener{
onPhotoListener?.onPhotoClick(position)
}
imageView.loadUrlImage(photos!![position]?.photo)
container.addView(itemView)
return itemView
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
container.removeView(`object` as FrameLayout)
}
}
The swipe on the custom viewpager doesn't work if the ImageView has this listener. If I remove the listener, works fine.
Anybody know what can be the problem?
Finally I solved the problem. I was to add logic to onInterceptTouchEvent to release the onTouch event if user only tap the viewpager and not swapping. This is the final code:
class VerticalViewPager : ViewPager {
var originalPosY = 0
#JvmOverloads
constructor(
context: Context,
attrs: AttributeSet? = null)
: super(context, attrs){
setPageTransformer(false, DefaultTransformer())
}
private fun swapTouchEvent(event: MotionEvent): MotionEvent {
val width = width.toFloat()
val height = height.toFloat()
val swappedX = event.y / height * width
val swappedY = event.x / width * height
event.setLocation(swappedX, swappedY)
return event
}
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
var y : Int = event.rawY.toInt()
return when (event.action) {
MotionEvent.ACTION_DOWN -> {
originalPosY = y
val intercept = super.onInterceptTouchEvent(swapTouchEvent(event))
swapTouchEvent(event)
false
}
MotionEvent.ACTION_MOVE -> {
val intercept = super.onInterceptTouchEvent(swapTouchEvent(event))
val i = swapTouchEvent(event)
true
}
MotionEvent.ACTION_UP -> {
if (Math.abs(originalPosY - y) > 10) {
super.onInterceptTouchEvent(swapTouchEvent(event))
val i = swapTouchEvent(event)
true
}else
false
}
else -> super.onInterceptTouchEvent(swapTouchEvent(event))
}
}
override fun onTouchEvent(event: MotionEvent): Boolean = super.onTouchEvent(swapTouchEvent(event))
}
The result is not all accurate that I want. If someone can improve it, it will be welcome