How to make one side semi circled background textView? - android

I would like to make such background for my textView:
I managed to make only with outer semi circle:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#color/p_color" />
<corners android:radius="24dp" />
</shape>
which is not good for me. I also tried to work with states list but didn't succeed with it.

Hello my Friend,
What you need will not create by shape Drawable, you need a custome view
in fact you need some trick to make your desired shape by custom views and use it inside a constraintlayout as background of your TextView
I coded for you by kotlin(if you need java inform me):
first:
make a file and name it CustomeBg copy below code put in that:
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
class CustomeBg #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var W = 0.0f
private var H = 0.0f
/////main body Rect and Paint
private var bodyRect = Rect()
private var bodyRectPaint = Paint(Paint.ANTI_ALIAS_FLAG)
//right half circle Rect and Paint
private var rightCircleRect = RectF()
private var rightCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG)
//left half circle Rect and Paint
private var leftCircleRect = RectF()
private var leftCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG)
init {
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
W = w.toFloat()
H = h.toFloat()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// drawRect(canvas)
var left = 0
var top = 0
var right = W - RADIUS_RIGHT
var bottom = H
bodyRectPaint.apply {
color = BODY_COLOR_RECT
style = Paint.Style.FILL
}
bodyRect.set(left, top, right.toInt(), bottom.toInt())
canvas.drawRect(bodyRect, bodyRectPaint)
// Draw Left half-Circle
var left_l_circle = -RADIUS_LEFT
var top_l_circle = 0f
var right_l_circle = RADIUS_LEFT
var bottom_l_circle = H
leftCirclePaint.apply {
color = LEFT_CIRCLE_COLOR
style = Paint.Style.FILL
}
leftCircleRect = RectF(left_l_circle, top_l_circle, right_l_circle, bottom_l_circle)
canvas.drawArc(leftCircleRect, -90f, 180f, true, leftCirclePaint)
// Draw Right half-Circle
var left_r_circle = (bodyRect.right) - RADIUS_RIGHT
var top_r_circle = 0f
var right_r_circle = bodyRect.right + RADIUS_RIGHT
var bottom_r_circle = H
rightCirclePaint.apply {
color = RIGHT_CIRCLE_COLOR
style = Paint.Style.FILL
}
rightCircleRect = RectF(left_r_circle, top_r_circle, right_r_circle, bottom_r_circle)
canvas.drawArc(rightCircleRect, -90f, 180f, true, rightCirclePaint)
}
companion object {
private val BODY_COLOR_RECT = Color.parseColor("#00534b")
private val LEFT_CIRCLE_COLOR = Color.parseColor("#ffffff")
private val RIGHT_CIRCLE_COLOR = Color.parseColor("#00534b")
private val RADIUS_LEFT = 100f
private val RADIUS_RIGHT = 100f
}
}
second:
then in your main_activity layout put below XML codes:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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"
tools:context=".MainActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout_editor_absoluteX="51dp"
tools:layout_editor_absoluteY="95dp">
<com.example.junk2.CustomeBg
android:id="#+id/itemSettingBg"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</com.example.junk2.CustomeBg>
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hallo ich bin Custome"
android:textColor="#FFFFFF"
app:layout_constraintBottom_toBottomOf="#+id/itemSettingBg"
app:layout_constraintEnd_toEndOf="#+id/itemSettingBg"
app:layout_constraintStart_toStartOf="#+id/itemSettingBg"
app:layout_constraintTop_toTopOf="#+id/itemSettingBg" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Result is :
I need explain a little my codes for better understanding:
in CustomeBg there are some value:
companion object {
private val BODY_COLOR_RECT = Color.parseColor("#00534b")
private val LEFT_CIRCLE_COLOR = Color.parseColor("#ffffff")
private val RIGHT_CIRCLE_COLOR = Color.parseColor("#00534b")
private val RADIUS_LEFT = 100f
private val RADIUS_RIGHT = 100f
}
in this file in fact we have 3 object, two half-circle(one for left and one for right)
and we have one rectangular as our body, above code explain radius for every circle and their colors, you can see body color here too
Notice: you can change above value for your usage my freiend

Related

How to set gradient in stroke android drawable

I am trying to create a circle drawable with a dash. I am able to achieve circle with dash but am not able to apply gradient color. Is there any way to do it? thanks in advance.
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size
android:width="#dimen/size60"
android:height="#dimen/size60" />
<stroke
android:width="1dp"
android:color="#color/white"
android:dashWidth="3dp"
android:dashGap="1dp" />
</shape>
View achieved:
View Required:
There is not a way to create a gradient ring using just XML AFAIK. You'll have better luck using a custom drawable. The following combine a sweep gradient shader with a Paint object to create a ring that has a gradient from start to end.
class DashedRingDrawable : Drawable() {
private val mPaint = Paint().apply {
style = Paint.Style.STROKE
strokeWidth = STROKE_WIDTH
}
private val mColorArray = intArrayOf(Color.WHITE, Color.BLACK)
private var mRingOuterDiameter = 0f
private var mRingOuterRadius = 0f
private var mRingInnerRadius = 0f
override fun onBoundsChange(bounds: Rect) {
super.onBoundsChange(bounds)
check(bounds.width() == bounds.height()) {
"Width must be equal to height. (It's a circle.)"
}
mRingOuterDiameter = bounds.width().toFloat()
mRingOuterRadius = mRingOuterDiameter / 2
mRingInnerRadius = (mRingOuterDiameter - STROKE_WIDTH) / 2
val dashLength = getNewDashLength()
mPaint.pathEffect = DashPathEffect(floatArrayOf(dashLength, GAP_LENGTH), 0f)
mPaint.shader = SweepGradient(mRingOuterRadius, mRingOuterRadius, mColorArray, null)
}
override fun draw(canvas: Canvas) {
// The following statement is here to show the boundaries and can be removed/commented out.
canvas.drawColor(Color.RED)
canvas.drawCircle(mRingOuterRadius, mRingOuterRadius, mRingInnerRadius, mPaint)
}
override fun setAlpha(alpha: Int) {
}
override fun setColorFilter(colorFilter: ColorFilter?) {
}
override fun getOpacity(): Int {
return PixelFormat.OPAQUE
}
// Adjust the dash length so that we end on a gap and not in the middle of a dash.
private fun getNewDashLength(): Float {
val circumference = Math.PI.toFloat() * mRingInnerRadius * 2
val dashCount = (circumference / (DASH_LENGTH + GAP_LENGTH)).toInt()
val newDashLength = (circumference - dashCount * GAP_LENGTH) / dashCount
return newDashLength
}
companion object {
const val STROKE_WIDTH = 15f
const val DASH_LENGTH = 50f
const val GAP_LENGTH = 15f
}
}
For API 24 and higher, you can place this drawable into an XML file and use it like any other XML drawable.
<drawable xmlns:android="http://schemas.android.com/apk/res/android"
class="com.example.myapp.DashedRingDrawable"/>
For APIs before API 24, you will need to work with this custom drawable programmatically.

Different way to show the image from the device, instead of resources using BitmapFactory

I'm making a small project, but yet different. So I found out that the bitmap works when the image is implemented from Android Studio and include it as resources(example: BitmapFactory.decodeResources(resources, R.drawable.image.jpg)) in order for the watermark to work, however I am struggling with something similar, but selecting a certain image from the android device, using by code of course, but later one probably gonna be questions once the gallery is selected to automatically place the watermark, but like I said that's a different question for another time. My question is there any possible way, when I press the watermark button, to automatically locate and place the image from the Android device on to the imageview, after that to place the text watermark ?
Also on the console log, I'm receiving this message: E/BitmapFactory: Unable to decode stream: java.io.FileNotFoundException: /data/user/0/com.example.cameraxapp/files (Is a directory)
If any of you know the issue that I am having, please let me know and examples, will also be helpful. Thank you in advance!
Manifest
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
ActivityGallery.xml(It can be even in MainActivity.xml)
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:background="#8A8A8A"
android:backgroundTint="#8A8A8A"
tools:context=".ActivityGallery">
<Button
android:id="#+id/watermarkSetBtn"
android:layout_width="75dp"
android:layout_height="wrap_content"
android:backgroundTint="#4CAF50"
android:text="#string/watermark"
android:textSize="11sp"
app:layout_constraintTop_toBottomOf="#id/imageViewPhoto"
app:layout_constraintEnd_toStartOf="#id/saveButton"
app:layout_constraintStart_toEndOf="#id/pickPhotoButton"
app:layout_constraintBottom_toBottomOf="parent"
/>
<ImageView
android:id="#+id/imageViewPhoto"
android:layout_width="match_parent"
android:layout_height="670dp"
android:importantForAccessibility="no"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="#android:drawable/ic_menu_gallery" />
</androidx.constraintlayout.widget.ConstraintLayout>
GalleryActivity.kt
The main function for the watermark as Bitmap
private fun addWatermark(bitmap: Bitmap, watermarkText: String, options: WatermarkOptions = WatermarkOptions()): Bitmap {
val result = bitmap.copy(bitmap.config, true)
val canvas = Canvas(result)
val paint = Paint(ANTI_ALIAS_FLAG or DITHER_FLAG)
paint.textAlign = when (options.corner) {
Corner.TOP_LEFT,
Corner.BOTTOM_LEFT -> Paint.Align.LEFT
Corner.TOP_RIGHT,
Corner.BOTTOM_RIGHT -> Paint.Align.RIGHT
}
val textSize = result.width * options.textSizeToWidthRatio
paint.textSize = textSize
paint.color = options.textColor
if (options.shadowColor != null) {
paint.setShadowLayer(textSize / 2, 0f, 0f, options.shadowColor)
}
if (options.typeface != null) {
paint.typeface = options.typeface
}
val padding = result.width * options.paddingToWidthRatio
val coordinates = calculateCoordinates(watermarkText, paint, options, canvas.width, canvas.height, padding)
canvas.drawText(watermarkText, coordinates.x, coordinates.y, paint)
return result
}
private fun calculateCoordinates(watermarkText: String, paint: Paint, options: WatermarkOptions, width: Int, height: Int, padding: Float): PointF {
val x = when (options.corner) {
Corner.TOP_LEFT,
Corner.BOTTOM_LEFT -> {
padding
}
Corner.TOP_RIGHT,
Corner.BOTTOM_RIGHT -> {
width - padding
}
}
val y = when (options.corner) {
Corner.BOTTOM_LEFT,
Corner.BOTTOM_RIGHT -> {
height - padding
}
Corner.TOP_LEFT,
Corner.TOP_RIGHT -> {
val bounds = Rect()
paint.getTextBounds(watermarkText, 0, watermarkText.length, bounds)
val textHeight = bounds.height()
textHeight + padding
}
}
return PointF(x, y)
}
enum class Corner {
TOP_LEFT,
TOP_RIGHT,
BOTTOM_LEFT,
BOTTOM_RIGHT
}
data class WatermarkOptions(
val corner: Corner = Corner.BOTTOM_RIGHT,
val textSizeToWidthRatio: Float = 0.04f,
val paddingToWidthRatio: Float = 0.03f,
#ColorInt val textColor: Int = Color.WHITE,
#ColorInt val shadowColor: Int? = Color.BLACK,
val typeface: Typeface? = null
)
companion object {
//private const val FILENAME_FORMAT = "yyyy.MM.dd-HH"
private const val TAG = "CameraXBasic"
}
Placing it on onCreate method
private lateinit var mImageView: ImageView
private lateinit var mWatermarkSet: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_gallery)
//Views
mImageView = findViewById(R.id.imageViewPhoto)
mWatermarkSet = findViewById(R.id.watermarkSetBtn)
mWatermarkSet.setOnClickListener {
if (filesDir.isDirectory) {
val internalStorage = filesDir.absolutePath
val pathName = "$internalStorage"
//resources, R.drawable.gefvqkbbslamdpkxf1zv_bigstock_aerial_view_of_blue_lakes_and__227291596
val originalBitmap = BitmapFactory.decodeFile(pathName)?.let { it ->
addWatermark(
it,
"#Test",
WatermarkOptions(
Corner.TOP_RIGHT,
textSizeToWidthRatio = 0.03f,
paddingToWidthRatio = 0.03f,
Color.GREEN,
Color.BLACK,
typeface = null
)
)
}
mImageView.setImageBitmap(originalBitmap)
}
}
}

Bottom Navigation is not pinning to bottom inside fragment view

Helo every one. I want to create a fragment that contains a bottom navigation bar. Bottom navigation is working but seeming wrong place in my fragment layout. An also this codes are working so good in activity but not in fragment.
Here is my fragment that I wanted to show bottom navigation bar.
class ParametreIslemlerFragment:Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState);
val mContext: Context = activity!!.applicationContext;
val view:View = inflater.inflate(R.layout.fragment_parametre_islemler,container,false);
val navBottomMenu:CurvedBottomNavigationView = view.findViewById(R.id.parametreBottomNavigation);
initNavBottomPreferences(navBottomMenu);
//fragmentManager?.beginTransaction()?.replace(R.id.parametreFragmentTutucu,FragmentBirinci())?.commit();
navBottomMenu.setOnNavigationItemSelectedListener { menuItem ->
true;
}
return view;
}
private fun initNavBottomPreferences(navBottomMenu:CurvedBottomNavigationView){
navBottomMenu.inflateMenu(R.menu.parametre_menu_nav_items);
navBottomMenu.labelVisibilityMode = LabelVisibilityMode.LABEL_VISIBILITY_LABELED;
navBottomMenu.menu.getItem(1).isVisible = false;
}
}
This fragment uses above layout;
<?xml version="1.0" encoding="utf-8"?>
<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">
<FrameLayout
android:id="#+id/parametreFragmentTutucu"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="30dp"
app:layout_constraintBottom_toTopOf="#+id/parametreBottomNavigation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</FrameLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/floatingActionButton3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
app:layout_constraintEnd_toEndOf="parent"
android:backgroundTint="#color/colorAccent"
android:layout_marginTop="1dp"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/parametreFragmentTutucu"
app:srcCompat="#drawable/ic_add_24dp" />
<com.mesutemre.kutuphanesistemi.customcomponents.CurvedBottomNavigationView
android:id="#+id/parametreBottomNavigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
</com.mesutemre.kutuphanesistemi.customcomponents.CurvedBottomNavigationView>
</androidx.constraintlayout.widget.ConstraintLayout>
CurvedBottomNavigation is a custom view and extends BottomNavigationView. When I run these codes ;
The codes are working in activity , why don't seems like activity layout in fragment?
Source code of CurvedBottomNavigationView is ;
class CurvedBottomNavigationView(context: Context, attrs:
AttributeSet)
:BottomNavigationView(context, attrs) {
private lateinit var mPath: Path;
private lateinit var mPaint: Paint;
private val CURVE_CIRCLE_RADIUS = 110 / 2;
private var mFirstCurveStartPoint: Point = Point();
private var mFirstCurveEndPoint: Point = Point();
private var mFirstCurveControlPoint1: Point = Point();
private var mFirstCurveControlPoint2: Point = Point();
private var mSecondCurveStartPoint: Point = Point();
private var mSecondCurveEndPoint: Point = Point();
private var mSecondCurveControlPoint1: Point = Point();
private var mSecondCurveControlPoint2: Point = Point();
private var mNavigationBarWidth: Int = 0;
private var mNavigationBarHeight: Int = 0;
init {
this.init();
}
private fun init(): Unit {
mPath = Path();
mPaint = Paint();
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
val colors = IntArray(3);
colors[0] = ContextCompat.getColor(
context,
R.color.bottom_start_color
);
colors[1] = ContextCompat.getColor(
context,
R.color.bottom_center_color
);
colors[2] = ContextCompat.getColor(
context,
R.color.bottom_end_color
);
val positions = FloatArray(3); //floatArrayOf(0f, 0.3f, 0.6f);
positions[0] = 0f;
positions[1] = 0.2f;
positions[2] = 0.4f;
mPaint.setShader(
LinearGradient(
0f, 0f, measuredWidth.toFloat(), 0f,
colors,
positions,
Shader.TileMode.CLAMP
)
);
mPaint.setShader(
LinearGradient(
0f, 0f, 0f, 250f,
colors, positions,
Shader.TileMode.MIRROR
)
);
//mPaint.setColor(ContextCompat.getColor(getContext(), R.color.primaryTextColor));
setBackgroundColor(Color.TRANSPARENT);
//background = resources.getDrawable(R.drawable.nav_bottom_background);
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh);
mNavigationBarWidth = getWidth();
mNavigationBarHeight = getHeight();
mFirstCurveStartPoint.set(
(mNavigationBarWidth / 2) - (CURVE_CIRCLE_RADIUS * 2) - (CURVE_CIRCLE_RADIUS / 3),
0
);
mFirstCurveEndPoint.set(
mNavigationBarWidth / 2,
CURVE_CIRCLE_RADIUS + (CURVE_CIRCLE_RADIUS / 4)
);
// same thing for the second curve
mSecondCurveStartPoint = mFirstCurveEndPoint;
mSecondCurveEndPoint.set(
(mNavigationBarWidth / 2) + (CURVE_CIRCLE_RADIUS * 2) + (CURVE_CIRCLE_RADIUS / 3),
0
);
mFirstCurveControlPoint1.set(
mFirstCurveStartPoint.x + CURVE_CIRCLE_RADIUS + (CURVE_CIRCLE_RADIUS / 4),
mFirstCurveStartPoint.y
);
// the coordinates (x,y) of the 2nd control point on a cubic curve
mFirstCurveControlPoint2.set(
mFirstCurveEndPoint.x - (CURVE_CIRCLE_RADIUS * 2) + CURVE_CIRCLE_RADIUS,
mFirstCurveEndPoint.y
);
mSecondCurveControlPoint1.set(
mSecondCurveStartPoint.x + (CURVE_CIRCLE_RADIUS * 2) - CURVE_CIRCLE_RADIUS,
mSecondCurveStartPoint.y
);
mSecondCurveControlPoint2.set(
mSecondCurveEndPoint.x - (CURVE_CIRCLE_RADIUS + (CURVE_CIRCLE_RADIUS / 4)),
mSecondCurveEndPoint.y
);
mPath.reset();
mPath.moveTo(0F, 0F);
mPath.lineTo(mFirstCurveStartPoint.x.toFloat(), mFirstCurveStartPoint.y.toFloat());
mPath.cubicTo(
mFirstCurveControlPoint1.x.toFloat(), mFirstCurveControlPoint1.y.toFloat(),
mFirstCurveControlPoint2.x.toFloat(), mFirstCurveControlPoint2.y.toFloat(),
mFirstCurveEndPoint.x.toFloat(), mFirstCurveEndPoint.y.toFloat()
);
mPath.cubicTo(
mSecondCurveControlPoint1.x.toFloat(), mSecondCurveControlPoint1.y.toFloat(),
mSecondCurveControlPoint2.x.toFloat(), mSecondCurveControlPoint2.y.toFloat(),
mSecondCurveEndPoint.x.toFloat(), mSecondCurveEndPoint.y.toFloat()
);
mPath.lineTo(mNavigationBarWidth.toFloat(), 0F);
mPath.lineTo(mNavigationBarWidth.toFloat(), mNavigationBarHeight.toFloat());
mPath.lineTo(0F, mNavigationBarHeight.toFloat());
mPath.close();
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
}
#SuppressLint("ResourceAsColor")
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas);
canvas?.drawPath(mPath, mPaint);
}
}
You are ignoring attributes and def styles. Extend view classes with #JvmOverloads annotation
class CurvedBottomNavigationView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : BottomNavigationView(context, attrs, defStyleAttr) {
// custom class body
}

When I use R.styleable in a custom view class, I get a red Unresolved reference: styleable

I am a beginner. I have started learning custom views these days, and there are almost no problems in the process.
When I went to Google to solve this problem, some people proposed solutions, but none of them was successful. I use a custom view written by Kotlin.
This is my custom view class,and the name is MyView.kt
package com.example.demos
import android.R
import android.content.Context
import android.content.res.TypedArray
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import kotlin.math.min
class MyView : View {
// init
private lateinit var arcPaint: Paint
private lateinit var progressTextPaint: Paint
// private lateinit var arcPaintColor: Color
private var arcPaintColor = Color.BLACK
// private lateinit var progressTextPaintColor: Color
private var progressTextPaintColor = Color.BLACK
private var angle = 0f
private var progress: Float = angle / 3.6f
// get/set
fun setArcPaintColor(color: Int) {
arcPaintColor = color
}
fun getArcPaintColor(): Int {
return arcPaintColor
}
fun setProgressTextPaintColor(color: Int) {
progressTextPaintColor = color
}
fun getProgressTextPaintColor(): Int {
return progressTextPaintColor
}
fun setAngle(float: Float) {
angle = float
progress = angle / 3.6f
invalidate()
}
fun getAngle(): Float {
return angle
}
fun setProgress(float: Float) {
progress = float
angle = progress * 3.6f
invalidate()
}
fun getProgress(): Float {
return progress
}
/*call method initPaint()*/
constructor(context: Context) : super(context) {
initPaint()
}
constructor(context: Context, attributeSet: AttributeSet?) : super(context, attributeSet) {
initPaint()
}
constructor(context: Context, attributeSet: AttributeSet?, defStyleAttr: Int) : super(
context,
attributeSet,
defStyleAttr
) {
arcPaintColor = typedArray.getColor(R.styleable.arcPaintColor,)
initPaint()
}
/*override onDraw(),draw view*/
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
drawView(canvas)
}
//init paints
private fun initPaint() {
arcPaint = Paint(Paint.ANTI_ALIAS_FLAG).also {
it.color = arcPaintColor
it.strokeWidth = 5f
it.strokeWidth = 40f
it.style = Paint.Style.STROKE
it.strokeCap = Paint.Cap.ROUND
}
progressTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).also {
it.color = progressTextPaintColor
// it.color = Color.GREEN
// it.setStrokeWidth(5f)
it.style = Paint.Style.FILL
it.textSize = 50f
}
}
/*draw view*/
private fun drawView(canvas: Canvas?) {
val displayWidth = width
val displayHeight = height
/*get center of circle*/
val centerX = (displayWidth / 2).toFloat()
val centerY = (displayHeight / 2).toFloat()
/*get radius*/
val radius = min(displayWidth, displayHeight) / 4
val rectF = RectF(
centerX - radius,
centerY - radius,
centerX + radius,
centerY + radius
)
canvas?.drawArc(
rectF,
0f,
angle,
false,
arcPaint
)
canvas?.drawText(
"${String.format("%.1f", progress)}%",
centerX - progressTextPaint.measureText("${String.format("%.1f", progress)}%") / 2,
centerY,
progressTextPaint
)
}
}
This is my xml file of custom attributes
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyView">
<attr name="arcPaintColor" format="color"/>
<attr name="progressTextPaintColor" format="color"/>
</declare-styleable>
</resources>
the xml file of my custom attribute
The issue is that you are importing android.R
You need to import the [you package name].R version instead.
You are not picking up the custom attributes correctly. This is how it should be done:
val typedArray =
context.theme.obtainStyledAttributes(
attributeSet, R.styleable.MyView, 0, 0
)
arcPaintColor = typedArray.getColor(R.styleable.MyView_arcPaintColor, 0)
typedArray.recycle() // Important!
initPaint()
You will have to make sure that this code executes in each of your constructors. The first zero will be replace by defStyleAttr when that is available. I suggest that you integrate the above code into initPaint().
See the documentation for obtainStyledAttributes().

Dotted underline TextView not wrapping to the next line using SpannableString in Android

I done the dotted underline textview using this Dotted underline in TextView using SpannableString in Android. But dotted underline textview not wrapping to the next line. I have attached screenshot for reference. Please advice your ideas. Thanks
class DottedUnderlineSpan(mColor: Int, private val mSpan: String) : ReplacementSpan() {
private val paint: Paint
private var width: Int = 0
private var spanLength: Float = 0f
private val lengthIsCached = false
internal var strokeWidth: Float = 0f
internal var dashPathEffect: Float = 0f
internal var offsetY: Float = 0f
init {
strokeWidth = 5f
dashPathEffect = 4f
offsetY = 14f
paint = Paint()
paint.color = mColor
paint.style = Paint.Style.STROKE
paint.pathEffect = DashPathEffect(floatArrayOf(dashPathEffect, dashPathEffect), 0f)
paint.strokeWidth = strokeWidth
}
override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
width = paint.measureText(text, start, end).toInt()
return width
}
override fun draw(canvas: Canvas, text: CharSequence, start: Int,
end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
canvas.drawText(text, start, end, x, y.toFloat(), paint)
if (!lengthIsCached)
spanLength = paint.measureText(mSpan)
val path = Path()
path.moveTo(x, y + offsetY)
path.lineTo(x + spanLength, y + offsetY)
canvas.drawPath(path, this.paint)
}
}
*Set dotted line using SpannableStringbuilder *
DottedUnderlineSpan dottedUnderlineSpan = new DottedUnderlineSpan(underlineColor, dottedString);
strBuilder.setSpan(dottedUnderlineSpan, start, end, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
Error:
Expected:
The problem is that a ReplacementSpan cannot cross a line boundary. See Drawing a rounded corner background on text for more information on this issue.
You could use the solution from the blog post mentioned above, but we can simplify that solution based upon your requirements as follows:
Here is the general procedure:
Place Annotation spans around the text in the TextView that we want to underline.
Let the text be laid out and catch the TextView just before drawing using a predraw listener. At this point the text is laid out as it will be displayed on the screen.
Replace each Annotation span with one or more DottedUnderlineSpans ensuring that each underline span does not cross a line boundary.
Strip trailing white space from the ReplacementSpan since we don't want to underline trailing white space.
Replace the text in the TextView.
A little complicated, but it will allow the use of the DottedUnderlineSpan class. This may not be a 100% solution since the width of the ReplacementSpan may vary from the width of the text under certain circumstances.
I do, however, recommend that you use a custom TextView with annotations to mark the placement of the underlines. This is probably going to be the easiest to do and to understand and is unlikely to have unforeseen side effects. The general procedure is to mark the text with annotation spans as above, but interpret these annotation spans in the draw() function of a custom text view to produce the underlines.
I have put together a small project to demonstrate these methods. The output looks like the following for a TextView with no underlined text, one with underlined text using the DottedUnderlineSpan and one with underlined text in a custom TextView.
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var textView0: TextView
private lateinit var textView1: TextView
private lateinit var textView2: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textView0 = findViewById(R.id.textView0)
textView1 = findViewById(R.id.textView1)
textView2 = findViewById<UnderlineTextView>(R.id.textView2)
if (savedInstanceState != null) {
textView1.text = SpannableString(savedInstanceState.getCharSequence("textView1"))
removeUnderlineSpans(textView1)
textView2.text = SpannableString(savedInstanceState.getCharSequence("textView2"))
} else {
val stringToUnderline = resources.getString(R.string.string_to_underline)
val spannableString0 = SpannableString(stringToUnderline)
val spannableString1 = SpannableString(stringToUnderline)
val spannableString2 = SpannableString(stringToUnderline)
// Get a good selection of underlined text
val toUnderline = listOf(
"production or conversion cycle",
"materials",
"into",
"goods",
"production and conversion cycle, where raw materials are transformed",
"saleable finished goods."
)
toUnderline.forEach { str -> setAnnotation(spannableString0, str) }
textView0.text = spannableString0
toUnderline.forEach { str -> setAnnotation(spannableString1, str) }
textView1.setText(spannableString1, TextView.BufferType.SPANNABLE)
toUnderline.forEach { str -> setAnnotation(spannableString2, str) }
textView2.setText(spannableString2, TextView.BufferType.SPANNABLE)
}
// Let the layout proceed and catch processing before drawing occurs to add underlines.
textView1.viewTreeObserver.addOnPreDrawListener(
object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
textView1.viewTreeObserver.removeOnPreDrawListener(this)
setUnderlinesForAnnotations(textView1)
return false
}
}
)
}
// The following is used of the manifest file specifies
// <activity android:configChanges="orientation">; otherwise, orientation processing
// occurs in onCreate()
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
removeUnderlineSpans(textView1)
textView1.viewTreeObserver.addOnPreDrawListener(
object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
textView1.viewTreeObserver.removeOnPreDrawListener(this)
setUnderlinesForAnnotations(textView1)
return false
}
}
)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putCharSequence("textView1", textView1.text)
outState.putCharSequence("textView2", textView2.text)
}
private fun setAnnotation(spannableString: SpannableString, subStringToUnderline: String) {
val dottedAnnotation =
Annotation(ANNOTATION_FOR_UNDERLINE_KEY, ANNOTATION_FOR_UNDERLINE_IS_DOTTED)
val start = spannableString.indexOf(subStringToUnderline)
if (start >= 0) {
val end = start + subStringToUnderline.length
spannableString.setSpan(dottedAnnotation, start, end, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
}
}
private fun setUnderlinesForAnnotations(textView: TextView) {
val text = SpannableString(textView.text)
val spans =
text.getSpans(0, text.length, Annotation::class.java).filter { span ->
span.key == ANNOTATION_FOR_UNDERLINE_KEY
}
if (spans.isNotEmpty()) {
val layout = textView.layout
spans.forEach { span ->
setUnderlineForAnnotation(text, span, layout)
}
textView.setText(text, TextView.BufferType.SPANNABLE)
}
}
private fun setUnderlineForAnnotation(text: Spannable, span: Annotation, layout: Layout) {
// Offset of first character in span
val spanStart = text.getSpanStart(span)
// Offset of first character *past* the end of the span.
val spanEnd = text.getSpanEnd(span)
// text.removeSpan(span)
// The span starts on this line
val startLine = layout.getLineForOffset(spanStart)
// Offset of the line that holds the last character of the span. Since
// spanEnd is the offset of the first character past the end of the span, we need
// to subtract one in case the span ends at the end of a line.
val endLine = layout.getLineForOffset(spanEnd)
for (line in startLine..endLine) {
// Offset to first character of the line.
val lineStart = layout.getLineStart(line)
// Offset to the character just past the end of this line.
val lineEnd = layout.getLineEnd(line)
// segStart..segEnd covers the part of the span on this line.
val segStart = max(spanStart, lineStart)
var segEnd = min(spanEnd, lineEnd)
// Don't want to underline end-of-line white space.
while ((segEnd > segStart) and Character.isWhitespace(text[segEnd - 1])) {
segEnd--
}
if (segEnd > segStart) {
val dottedUnderlineSpan = DottedUnderlineSpan()
text.setSpan(
dottedUnderlineSpan, segStart, segEnd, Spanned.SPAN_INCLUSIVE_INCLUSIVE
)
}
}
}
private fun removeUnderlineSpans(textView: TextView) {
val text = SpannableString(textView.text)
val spans = text.getSpans(0, text.length, DottedUnderlineSpan::class.java)
spans.forEach { span ->
text.removeSpan(span)
}
textView.setText(text, TextView.BufferType.SPANNABLE)
}
companion object {
const val ANNOTATION_FOR_UNDERLINE_KEY = "underline"
const val ANNOTATION_FOR_UNDERLINE_IS_DOTTED = "dotted"
}
}
DottedUnderlineSpan
I reworked this a little.
class DottedUnderlineSpan(
lineColor: Int = Color.RED,
dashPathEffect: DashPathEffect =
DashPathEffect(
floatArrayOf(DASHPATH_INTERVAL_ON, DASHPATH_INTERVAL_OFF), 0f
),
dashStrokeWidth: Float = DOTTEDSTROKEWIDTH
) : ReplacementSpan() {
private val mPaint = Paint()
private val mPath = Path()
init {
with(mPaint) {
color = lineColor
style = Paint.Style.STROKE
pathEffect = dashPathEffect
strokeWidth = dashStrokeWidth
}
}
override fun getSize(
paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?
): Int {
return paint.measureText(text, start, end).toInt()
}
override fun draw(
canvas: Canvas, text: CharSequence, start: Int,
end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint
) {
canvas.drawText(text, start, end, x, y.toFloat(), paint)
val spanLength = paint.measureText(text.subSequence(start, end).toString())
val offsetY =
paint.fontMetrics.bottom - paint.fontMetrics.descent + TEXT_TO_UNDERLINE_SEPARATION
mPath.reset()
mPath.moveTo(x, y + offsetY)
mPath.lineTo(x + spanLength, y + offsetY)
canvas.drawPath(mPath, mPaint)
}
companion object {
const val DOTTEDSTROKEWIDTH = 5f
const val DASHPATH_INTERVAL_ON = 4f
const val DASHPATH_INTERVAL_OFF = 4f
const val TEXT_TO_UNDERLINE_SEPARATION = 3
}
}
UnderlineTextView
class UnderlineTextView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {
private val mPath = Path()
private val mPaint = Paint()
init {
with(mPaint) {
color = Color.RED
style = Paint.Style.STROKE
pathEffect =
DashPathEffect(
floatArrayOf(DASHPATH_INTERVAL_ON, DASHPATH_INTERVAL_OFF), 0f
)
strokeWidth = DOTTEDSTROKEWIDTH
}
}
override fun draw(canvas: Canvas) {
super.draw(canvas)
// Underline goes on top of the text.
if (text is Spanned && layout != null) {
canvas.withTranslation(totalPaddingStart.toFloat(), totalPaddingTop.toFloat()) {
drawUnderlines(canvas, text as Spanned)
}
}
}
private fun drawUnderlines(canvas: Canvas, allText: Spanned) {
val spans =
allText.getSpans(0, allText.length, Annotation::class.java).filter { span ->
span.key == ANNOTATION_FOR_UNDERLINE_KEY && span.value == ANNOTATION_FOR_UNDERLINE_IS_DOTTED
}
if (spans.isNotEmpty()) {
spans.forEach { span ->
drawUnderline(canvas, allText, span)
}
}
}
private fun drawUnderline(canvas: Canvas, allText: Spanned, span: Annotation) {
// Offset of first character in span
val spanStart = allText.getSpanStart(span)
// Offset of first character *past* the end of the span.
val spanEnd = allText.getSpanEnd(span)
// The span starts on this line
val startLine = layout.getLineForOffset(spanStart)
// Offset of the line that holds the last character of the span. Since
// spanEnd is the offset of the first character past the end of the span, we need
// to subtract one in case the span ends at the end of a line.
val endLine = layout.getLineForOffset(spanEnd - 1)
for (line in startLine..endLine) {
// Offset of first character of the line.
val lineStart = layout.getLineStart(line)
// The segment always start somewhere on the start line. For other lines, the segment
// starts at zero.
val segStart = if (line == startLine) {
max(spanStart, lineStart)
} else {
0
}
// Offset to the character just past the end of this line.
val lineEnd = layout.getLineEnd(line)
// segStart..segEnd covers the part of the span on this line.
val segEnd = min(spanEnd, lineEnd)
// Get x-axis coordinate for the underline to compute the span length. This is OK
// since the segment we are looking at is confined to a single line.
val startStringOnLine = layout.getPrimaryHorizontal(segStart)
val endStringOnLine =
if (segEnd == lineEnd) {
// If segment ends at the line's end, then get the rightmost position on
// the line not imcluding trailing white space which we don't want to underline.
layout.getLineRight(line)
} else {
// The segment's end is on this line, so get offset to end of the last character
// in the segment.
layout.getPrimaryHorizontal(segEnd)
}
val spanLength = endStringOnLine - startStringOnLine
// Get the y-coordinate for the underline.
val offsetY = layout.getLineBaseline(line) + TEXT_TO_UNDERLINE_SEPARATION
// Now draw the underline.
mPath.reset()
mPath.moveTo(startStringOnLine, offsetY)
mPath.lineTo(startStringOnLine + spanLength, offsetY)
canvas.drawPath(mPath, mPaint)
}
}
fun setUnderlineColor(underlineColor: Int) {
mPaint.color = underlineColor
}
companion object {
const val DOTTEDSTROKEWIDTH = 5f
const val DASHPATH_INTERVAL_ON = 4f
const val DASHPATH_INTERVAL_OFF = 4f
const val TEXT_TO_UNDERLINE_SEPARATION = 3f
const val ANNOTATION_FOR_UNDERLINE_KEY = "underline"
const val ANNOTATION_FOR_UNDERLINE_IS_DOTTED = "dotted"
}
}
activity_main.xml
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".MainActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:context=".MainActivity">
<TextView
android:id="#+id/Label0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Plain Text"
app:layout_constraintBottom_toTopOf="#+id/textView0"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="#+id/textView0"
android:layout_width="188dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="#DDD6D6"
android:paddingBottom="2dp"
android:text="#string/string_to_underline"
android:textAppearance="#style/TextAppearance.AppCompat.Body1"
app:layout_constraintBottom_toTopOf="#+id/label1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/Label0" />
<TextView
android:id="#+id/label1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="DottedUndelineSpan"
app:layout_constraintBottom_toTopOf="#+id/textView1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/textView0" />
<TextView
android:id="#+id/textView1"
android:layout_width="188dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="#DDD6D6"
android:paddingBottom="2dp"
android:text="#string/string_to_underline"
android:textAppearance="#style/TextAppearance.AppCompat.Body1"
app:layout_constraintBottom_toTopOf="#+id/label2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/label1" />
<TextView
android:id="#+id/label2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="UnderlineTextView"
app:layout_constraintBottom_toTopOf="#+id/textView2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/textView1" />
<com.example.dottedunderlinespan.UnderlineTextView
android:id="#+id/textView2"
android:layout_width="188dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="#DDD6D6"
android:paddingBottom="2dp"
android:text="#string/string_to_underline"
android:textAppearance="#style/TextAppearance.AppCompat.Body1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/label2" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
I made a simple example that I have posted on Github (https://github.com/jaindiv26/DottedTextSample). I've followed the Deva's approach and made some adjustment, it's working for multiple lines too. Check out this example.
Dotted underline in TextView using SpannableString in Android.
1. Make DottedLineSpan Common Class.
class DottedLineSpan extends ReplacementSpan {
private Paint p = new Paint();
private int mWidth;
private String mSpan;
private float mSpanLength = 0F;
private boolean mLengthIsCached = false;
private Float mOffsetY = 0f;
DottedLineSpan(int _color, String _spannedText, Context context){
float mStrokeWidth = context.getResources().getDimension(R.dimen.stroke_width);
float mDashPathEffect = context.getResources().getDimension(R.dimen.dash_path_effect);
mOffsetY = context.getResources().getDimension(R.dimen.offset_y);
p = new Paint();
p.setColor(_color);
p.setStyle(Paint.Style.STROKE);
p.setPathEffect(new DashPathEffect(new float[]{mDashPathEffect, mDashPathEffect}, 0));
p.setStrokeWidth(mStrokeWidth);
mSpan = _spannedText;
mSpanLength = _spannedText.length();
}
#Override
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
mWidth = (int) paint.measureText(text, start, end);
return mWidth;
}
#Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
canvas.drawText(text, start, end, x, y, paint);
if(!mLengthIsCached)
mSpanLength = paint.measureText(mSpan);
Path path = new Path();
path.moveTo(x, y + mOffsetY);
path.lineTo(x + mSpanLength, y + mOffsetY);
canvas.drawPath(path, this.p);
}
}
2. Use this code in your activity.
public class MainActivity extends AppCompatActivity {
private TextView textView;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.textView);
String string = "Android is a mobile operating system based on a modified version of the Linux kernel and other open source software, designed primarily for touchscreen mobile devices such as smartphones and tablets. ";
String textToUnderline = "modified version of the Linux kernel";
SpannableString text = new SpannableString(string);
int[] range = getStartingAndEndOfSentence(string, textToUnderline);
DottedLineSpan dottedLineSpan = new DottedLineSpan(R.color.colorPrimary, textToUnderline, this);
text.setSpan(dottedLineSpan, range[0], range[1], Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(text);
}
int[] getStartingAndEndOfSentence(String wholeString, String partOfAString) {
int[] range = new int[2];
String[] s1 = wholeString.split("\\s+");
String[] s2 = partOfAString.split("\\s+");
if (s2.length == 1) {
String word = s2[0];
range[0] = wholeString.indexOf(word);
range[1] = range[0] + word.length();
} else {
int length = 0;
for (int i = 0; i < s1.length; i++) {
length = length + s1[i].length() + 1;
if (s1[i].equals(s2[0])) {
if(s1[i+1].equals(s2[1])) {
range[0] = length - (s1[i].length() + 1);
range[1] = range[0] + partOfAString.length();
break;
}
}
}
}
return range;
}
}

Categories

Resources