With the Android SeekBar, you can normally drag the thumb to the left or to the right and the yellow progress color is to the left of the thumb. I want the exact opposite, essentially flipping the yellow progress color to the right of the thumb and flipping the entire SeekBar on the y-axis.
Can anyone point me in the right direction? Thanks!
After fiddling around with some code this is what I got and it seems to work pretty well. Hopefully it will help someone else in the future.
public class ReversedSeekBar extends SeekBar {
public ReversedSeekBar(Context context) {
super(context);
}
public ReversedSeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ReversedSeekBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected void onDraw(Canvas canvas) {
float px = this.getWidth() / 2.0f;
float py = this.getHeight() / 2.0f;
canvas.scale(-1, 1, px, py);
super.onDraw(canvas);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
event.setLocation(this.getWidth() - event.getX(), event.getY());
return super.onTouchEvent(event);
}
}
This was thrown together with the help of these two questions:
How can you display upside down text with a textview in Android?
How can I get a working vertical SeekBar in Android?
Have you tried seekbar.setRotation( 180 )? It flips the seekbar 180 degrees and is upside down, meaning left side is max, right side is 0 with the color on the right of the thumb. No need to create a custom seekbar this way.
You should look into making a custom progress bar. Considering what you want to do, you already have the images you need in the Android SDK. I'd extract them and edit them accordingly. Here's a tutorial to help get you started.
Have you tried setting this in xml
android:rotationY="180"
This should fix a few issues with #mhenry answer
class ReverseSeekBar : SeekBar {
constructor(context: Context) : super(context) {
init()
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init()
}
constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) {
init()
}
var first = true
override fun onTouchEvent(event: MotionEvent): Boolean {
event.setLocation(this.width - event.x, event.y)
return super.onTouchEvent(event)
}
override fun getProgress(): Int {
return max - super.getProgress() + min
}
override fun setProgress(progress: Int) {
super.setProgress(max - progress + min)
}
override fun onDraw(canvas: Canvas?) {
if (first) {
first = false
val old = progress
progress = min + max - progress
super.onDraw(canvas)
progress = old
} else
super.onDraw(canvas)
}
private fun init() {
rotation = 180f
}
}
Related
I have created a custom view class extending Android's WebView, but when I add it to an xml layout, it appears as a gray block. I would like to show a custom layout in place of the gray block to be able to preview it.
The definition of the custom view is as follows:
class CustomView(context: Context, attrs: AttributeSet?) : WebView(context, attrs) {
constructor(context: Context) : this(context, null)
init {
if (isInEditMode) {
//show preview layout
context.getSystemService<LayoutInflater>()!!
.inflate(R.layout.widget_match_preview, this, true)
}
}
}
As can be seen in the code above, I have tried to show a preview in the init block, but it stills shows the gray block in the Android Studio layout preview.
AFAIK this can not be done, as #cactustictacs said, WebView implements its own drawing and can not be changed to another layout. What I ended up doing is overrride the onDraw(canvas: Canvas) method, and add the following to render a preview icon at the center of the view:
#SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
if (isInEditMode.not()) {
super.onDraw(canvas)
return
}
canvas.drawColor(Color.WHITE)
val bitmap = ResourcesCompat.getDrawable(resources, R.drawable.widget_preview, null)!!.toBitmap()
val size = minOf(width, height) / 2
val rect = Rect((width - size) / 2, (height - size) / 2, (width + size) / 2, (height + size) / 2)
canvas.drawBitmap(bitmap, null, rect, null)
}
Draw allocation is not a problem since will only be done once while in preview mode in the IDE. The only downside is that the icon is being drawn behind the "CustomView" text but is not big of a deal.
I think you're missing addView:
class CustomView(context: Context, attrs: AttributeSet?) : WebView(context, attrs) {
constructor(context: Context) : this(context, null)
init {
if (isInEditMode) {
// inflate the preview layout
val previewLayout = context.getSystemService<LayoutInflater>()!!
.inflate(R.layout.widget_match_preview, null)
// add the preview layout as a child view of the CustomView
addView(previewLayout)
}
}
}
You should:
class CustomView : LinearLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
init {
LayoutInflater.from(context).inflate(R.layout.widget_match_preview, this, true)
if (!isInEditMode) {
// skip code
}
}
}
Result:
In xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/red"/>
I am extending Chip class to perform some drawing over it for my lib , my use case is more complex but for simplicity let's say i am just drawing a diagonal line
my code
class MyChip (context: Context,attributeSet: AttributeSet) : Chip(context,attributeSet){
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//just want to draw a diagonal line
canvas.drawLine(0f,0f,width/1f,height/1f,paint)
}
}
xml
<com.abhinav.chouhan.loaderchipdemo.MyChip
android:layout_width="200dp"
android:layout_height="50dp"
android:textAlignment="center"
android:text="SOME TEXT"/>
when i don't have attribute android:textAlignment="center" everything works fine , but with that attribute we can not draw anything on chip.
I tried everything but couldn't figure out why is it happening.
Please Help
The Chip widget adheres strongly to the Material Design guidelines and is not (IMO) amenable to change. I suggest that you look at other widgets - maybe a MaterialButton to see if something else may suit your needs.
The attribute you reference, textAlignment, is available in TextView which is a class that Chip is based on. Chips expects wrap_content and also expect for text to be aligned to the start. Somewhere in the mix, your over-draw is lost. I doubt that something like that has ever been tested as it does not adhere to the Material guidelines.
However, if you must use a Chip, here is a way to do so with a custom view:
class MyChip #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : Chip(context, attrs, defStyleAttr) {
private val mLinePaint = Paint().apply {
color = Color.BLUE
strokeWidth = 10f
}
init {
doOnNextLayout {
// Turn off center alignment if specified. We will handle centering ourselves.
if (isTextAlignmentCenter()) {
textAlignment = View.TEXT_ALIGNMENT_GRAVITY
// Get the length of all the stuff before the text.
val lengthBeforeText =
if (isChipIconVisible) iconStartPadding + chipIconSize + iconEndPadding else 0f
val chipCenter = width / 2
// Chips have only one line, so we can get away with this.
val textWidth = layout.getLineWidth(0)
val newTextStartPadding = chipCenter - (textWidth / 2) - lengthBeforeText
textStartPadding = max(0f, newTextStartPadding)
textEndPadding = 0f
}
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//just want to draw a diagonal line
canvas.drawLine(0f, 0f, width.toFloat(), height.toFloat(), mLinePaint)
}
private fun isTextAlignmentCenter() = textAlignment == View.TEXT_ALIGNMENT_CENTER
}
A sample layout:
activity_main.xml
<com.example.myapplication.MyChip
android:id="#+id/chip"
android:layout_width="300dp"
android:layout_height="50dp"
android:layout_gravity="center"
android:text="Some Chip"
android:textAlignment="center"
app:chipIcon="#drawable/ic_baseline_cloud_download_24"
app:chipIconVisible="true"
app:closeIcon="#drawable/ic_baseline_clear_24"
app:closeIconVisible="true" />
</LinearLayout>
And the result:
I looked into the issue a little more deeply. For some reason, canvas operations such a drawLine(), drawRect(), etc. do not function. However, I can draw text on the canvas and fill it with a paint or a color.
Update: So, I tracked the problem down to the bringTextIntoView() method of TextView. For a reason that is not clear to me, the view is scrolled quite a few places (large, like 10's of thousands) in the positive direction. It is in this scrolled position that the text is written. To draw on the Chip, we must also scroll to this position. This scroll explains why I could fill the canvas with a color but could not draw on it so it would be visible.
The following will capture the "bad" scroll position and scroll to that position before drawing on the Chip. The screen capture looks the same as the above image.
MyChip.kt
class MyChip #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : Chip(context, attrs, defStyleAttr)/*, View.OnScrollChangeListener*/ {
private val mLinePaint = Paint().apply {
color = Color.BLUE
strokeWidth = 10f
}
private var mBadScroll = 0f
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//just want to draw a diagonal line
canvas.withTranslation(x = mBadScroll) {
canvas.drawLine(0f, 0f, this#MyChip.width.toFloat(), height.toFloat(), mLinePaint)
}
}
private fun isTextAlignmentCenter() = textAlignment == View.TEXT_ALIGNMENT_CENTER
override fun scrollTo(x: Int, y: Int) {
super.scrollTo(x, y)
if (isTextAlignmentCenter()) {
mBadScroll = scrollX.toFloat()
}
}
}
Update: Horizontal scrolling is still the issue. In onMeasure() method of TextView, the wanted width is set to VERY_WIDE if horizontal scrolling is enabled for the view. (And it is for Chip.) VERY_WIDE is 1024*1024 or one megabyte. This is the source of the large scroll that we see later on. This problem with Chip can be replicated directly with TextView if we call setHorizontallyScrolling(true) in the TextView.
Chips have only one line and, probably, shouldn't scroll. Calling setHorizontallyScrolling(true) doesn't make the Chip or TextView scrollable anyway. The above code now just becomes:
MyChip.kt
class MyChip #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : Chip(context, attrs, defStyleAttr) {
private val mLinePaint = Paint().apply {
color = Color.BLUE
strokeWidth = 10f
}
init {
setHorizontallyScrolling(false)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//just want to draw a diagonal line
canvas.drawLine(0f, 0f, this#MyChip.width.toFloat(), height.toFloat(), mLinePaint)
}
}
Horizontal scrolling is set to "true" in the constructor for Chip.
I am using a MotionLayout with a scene-xml:
<Transition
motion:constraintSetStart="#+id/start"
motion:constraintSetEnd="#+id/end"
>
<OnSwipe
motion:touchAnchorId="#+id/v_top_sheet"
motion:touchRegionId="#+id/v_top_sheet_touch_region"
motion:touchAnchorSide="bottom"
motion:dragDirection="dragDown" />
</Transition>
The 2 ConstraintSets are referencing only 2 View IDs: v_notifications_container and v_top_sheet.
In my Activity I want to set a normal ClickListener to one of the other Views in this MotionLayout:
iv_notification_status.setOnClickListener { Timber.d("Hello") }
This line is executed, but the ClickListener is never triggered. I searched other posts, but most of them deal with setting a ClickListener on the same View that is the motion:touchAnchorId. This is not the case here. The ClickListener is set to a View that is not once mentioned in the MotionLayout setup. If I remove the app:layoutDescription attribute, the click works.
I also tried to use setOnTouchListener, but it is also never called.
How can I set a click listener within a MotionLayout?
With the help of this great medium article I figured out that MotionLayout is intercepting click events even though the motion scene only contains an OnSwipe transition.
So I wrote a customized MotionLayout to only handle ACTION_MOVE and pass all other touch events down the View tree. Works like a charm:
/**
* MotionLayout will intercept all touch events and take control over them.
* That means that View on top of MotionLayout (i.e. children of MotionLayout) will not
* receive touch events.
*
* If the motion scene uses only a onSwipe transition, all click events are intercepted nevertheless.
* This is why we override onInterceptTouchEvent in this class and only let swipe actions be handled
* by MotionLayout. All other actions are passed down the View tree so that possible ClickListener can
* receive the touch/click events.
*/
class ClickableMotionLayout: MotionLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
if (event?.action == MotionEvent.ACTION_MOVE) {
return super.onInterceptTouchEvent(event)
}
return false
}
}
#muetzenflo's response is the most efficient solution I've seen so far for this problem.
However, only checking the Event.Action for MotionEvent.ACTION_MOVE causes the MotionLayout to respond poorly. It is better to differentiate between movement and a single click by the use of ViewConfiguration.TapTimeout as the example below demonstrates.
public class MotionSubLayout extends MotionLayout {
private long mStartTime = 0;
public MotionSubLayout(#NonNull Context context) {
super(context);
}
public MotionSubLayout(#NonNull Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
}
public MotionSubLayout(#NonNull Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if ( event.getAction() == MotionEvent.ACTION_DOWN ) {
mStartTime = event.getEventTime();
} else if ( event.getAction() == MotionEvent.ACTION_UP ) {
if ( event.getEventTime() - mStartTime <= ViewConfiguration.getTapTimeout() ) {
return false;
}
}
return super.onInterceptTouchEvent(event);
}
}
small modification of #TimonNetherlands code that works on the pixel4 aswell
class ClickableMotionLayout: MotionLayout {
private var mStartTime: Long = 0
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
if ( event?.action == MotionEvent.ACTION_DOWN ) {
mStartTime = event.eventTime;
}
if ((event?.eventTime?.minus(mStartTime)!! >= ViewConfiguration.getTapTimeout()) && event.action == MotionEvent.ACTION_MOVE) {
return super.onInterceptTouchEvent(event)
}
return false;
}
}
#Finn Marquardt's solution is efficient, but making a check only on ViewConfiguration.getTapTimeout() is not 100% reliable in my opinion and for me, sometimes the click event won't trigger because the duration of the tap is greater than getTapTimeout() (which is only 100ms). Also Long press is not handled.
Here is my solution, using GestureDetector:
class ClickableMotionLayout : MotionLayout {
private var isLongPressing = false
private var compatGestureDetector : GestureDetectorCompat? = null
var gestureListener : GestureDetector.SimpleOnGestureListener? = null
init {
setupGestureListener()
setOnTouchListener { v, event ->
if(isLongPressing && event.action == MotionEvent.ACTION_UP){
isPressed = false
isLongPressing = false
v.performClick()
} else {
isPressed = false
isLongPressing = false
compatGestureDetector?.onTouchEvent(event) ?: false
}
}
}
Where setupGestureListener() is implemented like this:
fun setupGestureListener(){
gestureListener = object : GestureDetector.SimpleOnGestureListener(){
override fun onLongPress(e: MotionEvent?) {
isPressed = progress == 0f
isLongPressing = progress == 0f
}
override fun onSingleTapUp(e: MotionEvent?): Boolean {
isPressed = true
performClick()
return true
}
}
compatGestureDetector = GestureDetectorCompat(context, gestureListener)
}
The GestureDetector handles the touch event only if it's a tap or if it's a long press (and it will manually trigger the "pressed" state). Once the user lifts the finger and the touch event is actually a long press, then a click event is triggered. In any other cases, MotionLayout will handle the event.
I'm afraid none of the other answers worked for me, I don't know it's because of an update in the libarary, but I don't get an ACTION_MOVE event on the region set in onSwipe.
Instead, this is what worked for me in the end:
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.view.children
/**
* This MotionLayout allows handling of clicks that clash with onSwipe areas.
*/
class ClickableMotionLayout #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MotionLayout(context, attrs, defStyleAttr) {
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
// Take all child views that are clickable,
// then see if any of those have just been clicked, and intercept the touch.
// Otherwise, let the MotionLayout handle the touch event.
if (children.filter { it.isClickable }.any {
it.x < event.x && it.x + it.width > event.x &&
it.y < event.y && it.y + it.height > event.y
}) {
return false
}
return super.onInterceptTouchEvent(event)
}
}
Basically, when we get a touch event, we iterate over all children of the MotionLayout and see if any of them (that are clickable) were the target of the event. If so, we intercept the touch event, and otherwise we let the MotionLayout do its thing.
This allows the user to click on clickable views that clashes with the onSwipe area, while also allowing swiping even if the swipe starts on the clickable view.
To setup onClick action on the view, use:
android:onClick="handleAction"
inside the MotionLayout file, and define "handleAction" in your class.
I'm trying to get the distance between the text and the left side of a TextView. It uses the property android:gravity="center".
I want to get the distance of the red bar (this red bar is not a part of the layout) to center the blue button. What should I do?
The dark area represents the bounds of the TextView.
I don't want to use a compoundDrawable because this view will change the color of the button randomly.
The code of the view (written in Kotlin):
class BallTextView: TextView {
private lateinit var ballPaint : Paint
private var ballRadius : Float = 10f
private var ballColor : Int = Color.BLACK
constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) {
initializeAttributes(attrs)
configBall()
}
override fun onDraw(canvas: Canvas) {
canvas.drawCircle(ballRadius, height.toFloat()/2, ballRadius, ballPaint)
super.onDraw(canvas)
}
fun configBall() {
ballPaint = Paint()
ballPaint.isAntiAlias = true
ballPaint.color = ballColor
}
fun initializeAttributes(attrs: AttributeSet) {
val attributes = context.obtainStyledAttributes(attrs, R.styleable.ball_textview)
ballRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
attributes.getFloat(R.styleable.ball_textview_ball_radius, ballRadius),
context.resources.displayMetrics)
ballColor = attributes.getColor(R.styleable.ball_textview_ball_color, ballColor)
}
}
Thanks.
The TextView#getLineBounds(int, Rect) method is what you want.
The first parameter is the zero-based line number, and the second is a Rect object that will hold the bounds values of the given line after the call. The left field of the Rect will have the horizontal inset of the line, which you can use with the radius of your drawn circle to figure its center's x coordinate.
My another solution was:
override fun onDraw(canvas: Canvas) {
if (xPosition == 0f) {
xPosition = (width - paint.measureText(text.toString())) / 3
}
canvas.drawCircle(xPosition, height.toFloat()/2, ballRadius, ballPaint)
super.onDraw(canvas)
}
#MikeM. What do you think about this approach?
Is there anyway to know when a progressbar has reached it maximum. Like a Listener then could plug into the ProgressBar and lets us know that the progress bar is at the maximum level ?
Kind Regards
There isn't a direct way to do this. A workaround could be to make a custom implementation of the ProgressBar and override the setProgress method:
public MyProgressBar extends ProgressBar
{
#Override
public void setProgress(int progress)
{
super.setProgress(progress);
if(progress == this.getMax())
{
//Do stuff when progress is max
}
}
}
I think the cleanest way would be just adding this to your code:
if (progressBar.getProgress() == progressBar.getMax()) {
// do stuff you need
}
If you need onProgressChanged like SeekBar - create custom progress (Kotlin):
class MyProgressBar : ProgressBar {
private var listener: OnMyProgressBarChangeListener? = null
fun setOnMyProgressBarChangeListener(l: OnMyProgressBarChangeListener) {
listener = l
}
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(
context,
attrs
)
constructor(
context: Context?,
attrs: AttributeSet?,
defStyleAttr: Int
) : super(context, attrs, defStyleAttr)
override fun setProgress(progress: Int) {
super.setProgress(progress)
listener?.onProgressChanged(this)
}
interface OnMyProgressBarChangeListener {
fun onProgressChanged(myProgressBar: MyProgressBar?)
}
}
And for example in your fragment:
progress_bar?.setOnMyProgressBarChangeListener(object :
MyProgressBar.OnMyProgressBarChangeListener {
override fun onProgressChanged(myProgressBar: MyProgressBar?) {
val p = progress_bar.progress
// do stuff like this
if (p != 100) {
percentCallback?.changePercent(p.toString()) // show animation custom view with grow percents
} else {
shieldView.setFinishAndDrawCheckMark() // draw check mark
}
}
})
I think overall you should never have to do this. Is there just one valid case where you need to listen to a progressbar progress? I mean, usually it's the other way around: you set the progressbar progress based on something, not the other way around, so you need to track the progress of that something instead of listening to a view (which may or may not exist by the way).