Exoplayer seekbar preview - android

I'm trying to add a preview to my seekbar on my exoplayer just like in youtube or plex (see the image below)
I've found this library but it isn't up-to-date yet.
I already have the image per frame but I don't know how to integrate them in my Exoplayer, I'm looking for either a tutorial or explanation where I should begin because I'm kind of lost there.
I've found Timebar.onScrubListener while browsing the exoplayer doc. I'm guessing I'll be using these 3 listeners to fetch the position of the scrub and display the corresponding image.

UPDATE: The library is up-to-date as of May 2020 so you can use it directly.
I'll leave code below for those who don't want to use the library.
After searching and adapting it to my needs I found a way by looking at how previewSeekBar was doing and I ended up using the same thing so here it is:
My sprite is composed of 10 columns and 6 rows, each square represent 1 second
GlideTransformation
private const val MAX_LINES = 6
private const val MAX_COLUMNS = 10
private const val THUMBNAILS_EACH = 1000 // milliseconds
private const val ONE_MINUTE = 60000 // one minute in millisecond
class GlideThumbnailTransformation(position: Long) : BitmapTransformation() {
private val x: Int
private val y: Int
init {
// Remainder of position on one minute because we just need to know which square of the current miniature
val square = position.rem(ONE_MINUTE).toInt() / THUMBNAILS_EACH
y = square / MAX_COLUMNS
x = square % MAX_COLUMNS
}
override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
val width = toTransform.width / MAX_COLUMNS
val height = toTransform.height / MAX_LINES
return Bitmap.createBitmap(toTransform, x * width, y * height, width, height)
}
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
val data: ByteArray = ByteBuffer.allocate(8).putInt(x).putInt(y).array()
messageDigest.update(data)
}
override fun hashCode(): Int {
return (x.toString() + y.toString()).hashCode()
}
override fun equals(other: Any?): Boolean {
if (other !is GlideThumbnailTransformation) {
return false
}
return other.x == x && other.y == y
}
}
Activity
val thumbnailUrl = "https://bitdash-a.akamaihd.net/content/MI201109210084_1/thumbnails/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.jpg"
exo_progress.addListener(object : TimeBar.OnScrubListener {
override fun onScrubMove(timeBar: TimeBar, position: Long) {
previewFrameLayout.visibility = View.VISIBLE
val targetX = updatePreviewX(position.toInt(), exoPlayer.duration.toInt())
previewFrameLayout.x = targetX.toFloat()
GlideApp.with(scrubbingPreview)
.load(thumbnailUrl)
.override(Target.SIZE_ORIGINAL,Target.SIZE_ORIGINAL)
.transform(GlideThumbnailTransformation(position))
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.into(scrubbingPreview)
}
override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
previewFrameLayout.visibility = View.INVISIBLE
}
override fun onScrubStart(timeBar: TimeBar, position: Long) {}
})
private fun updatePreviewX(progress: Int, max: Int): Int {
if (max == 0) { return 0 }
val parent = previewFrameLayout.parent as ViewGroup
val layoutParams = previewFrameLayout.layoutParams as MarginLayoutParams
val offset = progress.toFloat() / max
val minimumX: Int = previewFrameLayout.left
val maximumX = (parent.width - parent.paddingRight - layoutParams.rightMargin)
// We remove the padding of the scrubbing, if you have a custom size juste use dimen to calculate this
val previewPaddingRadius: Int = dpToPx(resources.displayMetrics, DefaultTimeBar.DEFAULT_SCRUBBER_DRAGGED_SIZE_DP).div(2)
val previewLeftX = (exo_progress as View).left.toFloat()
val previewRightX = (exo_progress as View).right.toFloat()
val previewSeekBarStartX: Float = previewLeftX + previewPaddingRadius
val previewSeekBarEndX: Float = previewRightX - previewPaddingRadius
val currentX = (previewSeekBarStartX + (previewSeekBarEndX - previewSeekBarStartX) * offset)
val startX: Float = currentX - previewFrameLayout.width / 2f
val endX: Float = startX + previewFrameLayout.width
// Clamp the moves
return if (startX >= minimumX && endX <= maximumX) {
startX.toInt()
} else if (startX < minimumX) {
minimumX
} else {
maximumX - previewFrameLayout.width
}
}
private fun dpToPx(displayMetrics: DisplayMetrics, dps: Int): Int {
return (dps * displayMetrics.density).toInt()
}
XML
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_gravity="bottom"
android:layoutDirection="ltr"
android:background="#CC000000"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="4dp"
android:orientation="horizontal"
android:id="#+id/controlsLayout"
app:layout_constraintBottom_toBottomOf="parent">
<ImageButton android:id="#id/exo_prev"
style="#style/ExoMediaButton.Previous"/>
<ImageButton android:id="#id/exo_rew"
style="#style/ExoMediaButton.Rewind"/>
<ImageButton android:id="#id/exo_repeat_toggle"
style="#style/ExoMediaButton"/>
<ImageButton android:id="#id/exo_play"
style="#style/ExoMediaButton.Play"/>
<ImageButton android:id="#id/exo_pause"
style="#style/ExoMediaButton.Pause"/>
<ImageButton android:id="#id/exo_ffwd"
style="#style/ExoMediaButton.FastForward"/>
<ImageButton android:id="#id/exo_next"
style="#style/ExoMediaButton.Next"/>
</LinearLayout>
<TextView android:id="#id/exo_position"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textStyle="bold"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:includeFontPadding="false"
android:textColor="#FFBEBEBE"
app:layout_constraintBottom_toTopOf="#id/controlsLayout"
app:layout_constraintStart_toStartOf="parent"/>
<FrameLayout
android:id="#+id/previewFrameLayout"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:background="#drawable/video_frame"
android:padding="2dp"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="#+id/exo_progress"
app:layout_constraintDimensionRatio="16:9"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintWidth_default="percent"
app:layout_constraintWidth_percent="0.25"
tools:visibility="visible">
<ImageView
android:id="#+id/scrubbingPreview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY" />
</FrameLayout>
<com.google.android.exoplayer2.ui.DefaultTimeBar
android:id="#id/exo_progress"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="26dp"
app:layout_constraintBottom_toBottomOf="#id/exo_position"
app:layout_constraintEnd_toStartOf="#id/exo_duration"
app:layout_constraintStart_toEndOf="#+id/exo_position"
app:layout_constraintTop_toTopOf="#+id/exo_position"/>
<TextView android:id="#id/exo_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textStyle="bold"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:includeFontPadding="false"
android:textColor="#FFBEBEBE"
app:layout_constraintBaseline_toBaselineOf="#id/exo_position"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
drawable/video_frame
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke
android:width="2dp"
android:color="#android:color/white" />
<solid android:color="#android:color/black" />
</shape>
there might be some improvement to make so feel free to comment

Related

Toast View with Progress Animation Android, Left to Right

Need a solution to add a Shake View with a progress bar. This view is used as a Toast View in Android, I got a solution such that the View grows from the center and needs help in transforming into the expected result.
The current state is zipped in the drive with the actual video & expected video.
Video and Current state source code
I tried this growFromCenter Extension function, I even tried using Object animator but got blocked while combining it with the progress bar.
private fun View.growFromCenter(
duration: Long,
#FloatRange(from = 0.0, to = 1.0)
startScaleRatio: Float,
endAnimCallback: () -> Unit = {},
) {
val growFromCenter = ScaleAnimation(
startScaleRatio,
1f,
startScaleRatio,
1f,
Animation.RELATIVE_TO_SELF,
0.5f,
Animation.RELATIVE_TO_SELF,
0.5f
)
growFromCenter.duration = duration
growFromCenter.fillAfter = true
growFromCenter.interpolator = FastOutSlowInInterpolator()
growFromCenter.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(p0: Animation?) {
}
override fun onAnimationEnd(p0: Animation?) {
endAnimCallback.invoke()
}
override fun onAnimationRepeat(p0: Animation?) {
}
})
startAnimation(growFromCenter)
}
Use SnackProgressBar instead, I think it is better and easier to manage.
Shake view with a progress bar
Step 1:
In res/anim create file animation.xml
<?xml version="1.0" encoding="utf-8"?>
<set
xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true">
<scale
android:duration="350"
android:fromXScale="0.0"
android:fromYScale="0.5"
android:pivotX="100%"
android:pivotY="50%"
android:toXScale="1.0"
android:toYScale="1.0" />
<translate
android:duration="175"
android:fromXDelta="0%"
android:repeatCount="5"
android:repeatMode="reverse"
android:toXDelta="7%" />
</set>
Step 2: In your require Activity
class MainActivity : AppCompatActivity() {
private val viewModel:MainActivityViewModel by viewModels()
private lateinit var mBinding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding=ActivityMainBinding.inflate(layoutInflater)
setContentView(mBinding.root)
viewModel.currentTime.observe(this) {
val seconds = TimeUnit.MILLISECONDS.toSeconds(it*100)- TimeUnit.MINUTES.toSeconds(
TimeUnit.MILLISECONDS.toMinutes(it*100)
)
val minutes = TimeUnit.MILLISECONDS.toMinutes(it*100)
Log.v("timmer", "$minutes : ${seconds}")
mBinding.progressBar.progress = it.toInt()
}
mBinding.btn.setOnClickListener {
mBinding.mtrlCardChecked.visibility= View.VISIBLE
mBinding.progressBar.visibility=View.VISIBLE
mBinding.mtrlCardChecked.startAnimation(AnimationUtils.loadAnimation(this, R.anim.animation))
viewModel.timer(15000L)
mBinding.progressBar.max=15*10
}
}}
Step 3: In ViewModel of your require activity
package com.example.toasty
import android.os.CountDownTimer
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class MainActivityViewModel : ViewModel() {
private var ONE_SEC = 100L
lateinit var mCountDownTimer: CountDownTimer
private val _currentTime: MutableLiveData<Long> = MutableLiveData<Long>(0)
val currentTime: LiveData<Long> = _currentTime
fun timer(countTimer: Long) {
mCountDownTimer = object : CountDownTimer(countTimer, ONE_SEC) {
override fun onTick(p0: Long) {
_currentTime.value = (p0 / ONE_SEC)
}
override fun onFinish() {
}
}
mCountDownTimer.start()
}
}
Step 4: In your activity_main.xml file
<?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:id="#+id/ctl"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.google.android.material.button.MaterialButton
android:id="#+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello Toasty!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/mtrl_card_checked"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="10dp"
android:visibility="gone"
app:cardCornerRadius="3dp"
app:cardElevation="12dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<View
android:id="#+id/v_bar"
android:layout_width="5dp"
android:layout_height="90dp"
android:background="#android:color/holo_green_light"
app:layout_constraintBottom_toTopOf="#id/progressBar"
app:layout_constraintStart_toStartOf="parent" />
<androidx.appcompat.widget.AppCompatImageView
android:id="#+id/iv_message_icon"
android:layout_width="56dp"
android:layout_height="56dp"
android:background="#android:color/holo_green_light"
android:src="#drawable/ic_launcher_foreground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="#id/v_bar"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageView
android:id="#+id/iv_message_icon_type"
android:layout_width="20dp"
android:layout_height="20dp"
android:padding="4dp"
app:layout_constraintBottom_toBottomOf="#id/iv_message_icon"
app:layout_constraintEnd_toEndOf="#id/iv_message_icon" />
<TextView
android:id="#+id/tv_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:text="You have too many players in the same position. To pick another, transfer one out. (2/2)"
app:layout_constraintBottom_toBottomOf="#id/iv_message_icon"
app:layout_constraintEnd_toStartOf="#id/iBtn_message_popup_close"
app:layout_constraintStart_toEndOf="#id/iv_message_icon"
app:layout_constraintTop_toTopOf="#id/iv_message_icon" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="#+id/iBtn_message_popup_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:background="?actionBarItemBackground"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="#+id/progressBar"
android:layout_width="0dp"
android:layout_height="50dp"
android:progress="10"
android:secondaryProgressTintMode="screen"
app:indicatorColor="#color/teal_200"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/v_bar"
app:trackColor="#color/purple_200"
app:trackThickness="10dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
You can directly download the sample project from Github repo
After making the Custom View, You can Use below code to show that with animation
val snackBarView = Snackbar.make(view, "SnackBar Message" , Snackbar.LENGTH_LONG)
val view = snackBarView.view
val params = view.layoutParams as FrameLayout.LayoutParams
params.gravity = Gravity.TOP
view.layoutParams = params
view.background = ContextCompat.getDrawable(context,R.drawable.custom_drawable) // for custom background
snackBarView.animationMode = BaseTransientBottomBar.ANIMATION_MODE_FADE
snackBarView.show()
below line will resolve the animation issue.
snackBarView.animationMode = BaseTransientBottomBar.ANIMATION_MODE_FADE
Alternate solution-
snackBarView.anchorView = mention viewId above whom you want to show SnackBar

Very long image being cut when the height of the image is not enough

Here are the 2 situations I have in my app -
as you can see, the image is being centered when it is being enlarged.
The wanted result is that I would like to have see the left side of the image always, meaning let's say the 40% left side of the image.
Here is my holder 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:id="#+id/vendors_row_item_root_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.cardview.widget.CardView
android:id="#+id/search_image_contact_cardview"
android:layout_width="152dp"
android:layout_height="match_parent"
android:layout_margin="5dp"
app:cardCornerRadius="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="#+id/vendors_row_item_imageview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
tools:src="#mipmap/ic_launcher" />
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
here is my holder class -
class VendorsHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var vendorImageView: ImageView = itemView.findViewById(R.id.vendors_row_item_imageview)
var rootLayout: ConstraintLayout = itemView.findViewById(R.id.vendors_row_item_root_layout)
var vendorHolderCardview: CardView = itemView.findViewById(R.id.search_image_contact_cardview)
}
adapter -
class VendorAdapter(private val miniVendorModels: List<MiniVendorModel>, private val context: Context) : RecyclerView.Adapter<VendorsHolder>() {
companion object {
const val EXTRA_VENDOR_MODEL = "EVM"
}
private val vendorsHoldersList = mutableListOf<VendorsHolder>()
override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): VendorsHolder {
val view = LayoutInflater.from(viewGroup.context).inflate(R.layout.fragment_marketplace_vendor_row_item, viewGroup, false)
val vendorsHolder = VendorsHolder(view)
vendorsHoldersList.add(vendorsHolder)
return vendorsHolder
}
override fun onBindViewHolder(vendorsHolder: VendorsHolder, i: Int) {
val model = miniVendorModels[i]
Picasso.get().load(model.bannerPicture).into(vendorsHolder.vendorImageView)
vendorsHolder.vendorImageView.setOnClickListener { v: View? ->
try {
val intent = Intent(context, VendorPageActivity::class.java)
intent.putExtra(EXTRA_VENDOR_MODEL, model)
context.startActivity(intent)
} catch (e: Exception) {
e.printStackTrace()
Toast.makeText(context, ResourceHelper.getString(R.string.marketplace_vendor_unavailable), Toast.LENGTH_SHORT).show()
}
}
}
override fun getItemCount(): Int = miniVendorModels.size
fun resizeAllHolders(height : Int){
vendorsHoldersList.forEach { holder ->
holder.rootLayout.updateLayoutParams {
this.height = height
}
}
}
}
any ideas of how to implement this one?
try changing
<ImageView
android:id="#+id/vendors_row_item_imageview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
tools:src="#mipmap/ic_launcher" />
to
<ImageView
android:id="#+id/vendors_row_item_imageview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter"
tools:src="#mipmap/ic_launcher" />

MDC Button MaterialShapeDrawable strange shadows and no ripple

Similar to the attachment in this issue, my button's corner shadows look pretty dodgy. But with a custom edge and rounded corner treatment, the button seems to lose elevation and there is no ripple / click effect. Any idea on what I have done wrong?
Material version is 1.1.0-alpha07
class CurvedEdgeTreatment (private val size: Float) : EdgeTreatment(), Cloneable {
public override fun clone(): EdgeTreatment {
return super<EdgeTreatment>.clone()
}
override fun getEdgePath(length: Float, center: Float, interpolation: Float, shapePath: ShapePath) {
shapePath.quadToPoint(center, -size * interpolation, length, 0f)
}
}
class ButtonActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_button)
val shapeAppearanceModel = ShapeAppearanceModel().apply {
setAllCorners(RoundedCornerTreatment(12.dpToPx(resources.displayMetrics).toFloat()))
setAllEdges(CurvedEdgeTreatment(3.dpToPx(resources.displayMetrics).toFloat()))
}
val backgroundDrawable = MaterialShapeDrawable(shapeAppearanceModel).apply {
setTint(ContextCompat.getColor(this#ButtonActivity, R.color.color_secondary))
shadowCompatibilityMode = SHADOW_COMPAT_MODE_ALWAYS
elevation = 12f
setUseTintColorForShadow(true)
paintStyle = Paint.Style.FILL
}
raisedContainedButton.background = backgroundDrawable
}
fun Int.dpToPx(displayMetrics: DisplayMetrics): Int = (this * displayMetrics.density).toInt()
fun Int.pxToDp(displayMetrics: DisplayMetrics): Int = (this / displayMetrics.density).toInt()
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:padding="16dp"
android:clipChildren="false"
android:clipToPadding="false">
<com.google.android.material.button.MaterialButton
android:id="#+id/raisedContainedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="I am a button la la la la la la"/>
</FrameLayout>
</LinearLayout>

Change constraint in recyclerview viewholder layout - android

I am working on chat app so I want time TextView to be like whats app it's constraint can change depend on the text
So I tried the view tree observer in the message layout and it somehow worked but there something missing
private fun adjustTimeTextView() {
var freeSpace: Float
var lines: Int
val messageViewObserver = messageView.messageLayout.viewTreeObserver
messageViewObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
messageView.messageLayout.viewTreeObserver.removeOnGlobalLayoutListener(this)
val viewWidth = messageView.messageLayout.width
Log.e("message view width", viewWidth.toString())
val layout = messageView.messageTextTextView.layout
if (layout != null) {
lines = layout.lineCount
Log.e("line", lines.toString())
val offset = layout.getLineWidth(lines - 1)
freeSpace = viewWidth - offset
if (freeSpace < 220) {
Log.e("minmum", "low free space")
val constraintSet = ConstraintSet()
constraintSet.clone(messageView.messageLayout)
constraintSet.clear(messageView.messageLayout.messageTimeTextView.id, ConstraintSet.TOP)
constraintSet.clear(messageView.messageLayout.messageTimeTextView.id, ConstraintSet.BOTTOM)
constraintSet.clear(messageView.messageLayout.messageTimeTextView.id, ConstraintSet.START)
constraintSet.connect(
messageView.messageLayout.messageTimeTextView.id,
ConstraintSet.TOP,
messageView.messageLayout.messageTextTextView.id,
ConstraintSet.BOTTOM
)
constraintSet.applyTo(messageView.messageLayout)
} else {
if (lines > 1) {
val constraintSet = ConstraintSet()
constraintSet.clone(messageView.messageLayout)
constraintSet.clear(
messageView.messageLayout.messageTimeTextView.id,
ConstraintSet.START
)
constraintSet.applyTo(messageView.messageLayout)
}
}
}
}
})
}
and here is my layout xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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="wrap_content"
>
<android.support.constraint.Guideline
android:id="#+id/endGuideline"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintGuide_percent="0.65"/>
<android.support.constraint.Guideline
android:id="#+id/startGuideline"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintGuide_percent="0.35"/>
<android.support.constraint.ConstraintLayout
android:id="#+id/messageLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constrainedWidth="true"
android:padding="4dp"
app:layout_constraintEnd_toStartOf="#+id/endGuideline"
android:background="#drawable/sender_message_background"
android:orientation="vertical"
app:layout_constraintWidth_default="wrap"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintTop_toBottomOf="#+id/messageDateTextView" android:layout_marginTop="8dp">
<TextView
android:id="#+id/messageTextTextView"
tools:text="hi"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0" android:layout_marginEnd="8dp"/>
<TextView
android:id="#+id/messageTimeTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="12:34 pm"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="#+id/messageTextTextView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#+id/messageTextTextView"
app:layout_constraintStart_toEndOf="#+id/messageTextTextView"
android:layout_marginStart="8dp" app:layout_constraintVertical_bias="0.51"/>
</android.support.constraint.ConstraintLayout>
</android.support.constraint.ConstraintLayout>
The problem is that when I open an existing chat the constraint as first image appear didn't work as expected in the long message but as soon as I click on type a message edit text the constraint changed for the screen chat not for all chat so if i scrolled the constraints of chat on screen is unchanged unless i click on type a message edit text and so on.
am i missing listener or request focus or something else or what is missing
Here is picture one
Here is picture two
In my opinion, the solution here is to use app:layout_constrainedWidth="true". Requesting layout is a heavy task and should not be performed on scroll.
it worked by make recyclerView request layout
i added this code to recyclerview adapter
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener(){
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
recyclerView.requestLayout()
}
})
}

How to add chips from Material Components library to input field in android?

I've seen that in android-P google add new material components library which contains material chips:
Material components for android
Material.io chips usage
Material components on GitHub
So I decided to add material input chips to my project, but unfortunately didn't find any tutorial how to make that. I want to create something like Gmail chips but without image on the start.
Because I'm using appcompat library I tried to use material chips by android.support.design.chip.Chip and android.support.design.chip.ChipGroup.
But result was just chips without any input field. Also I tried to create a Standalone ChipDrawable and then add it to EditText using
Editable text = editText.getText();
text.setSpan(span, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
But I got empty EditText without any chips. So how can I create chips input like in Gmail using this material components library? Maybe someone has expreience or knows any tutorials where I could see how to create this?
Thanks in advance!
Answer
No default input field for adding chips in android. They mentioned input chips but i didn't find any layout or viewgroup for input chips. So i do with Chipdrawable method to add chips in edittext. Here am using AppCompatEdittext you can change to anyview which listening the text inputs.
Reference.
Step 1
Add chip xml resource. chip.xml
res -> xml -> chip.xml
<?xml version="1.0" encoding="utf-8"?>
<chip xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:textAppearance="#style/ChipTextAppearance"
app:chipBackgroundColor="#color/colorAccent"
app:chipIcon="#drawable/ic_call_white_24dp"
app:closeIconEnabled="true" <!--property for close icon if no need set to false. -->
app:closeIconTint="#android:color/white" />
Then add textappearance style in style.xml(For change textStyle)
<style name="ChipTextAppearance" parent="TextAppearance.MaterialComponents.Chip">
<item name="android:textColor">#android:color/white</item>
</style>
Step 2
Add your view here am using AppCompatEdittext
<android.support.v7.widget.AppCompatEditText
android:id="#+id/phone"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/tvt_Contact" />
Step 3
Add this code to your view to get the desired behaviour.
private int SpannedLength = 0,chipLength = 4;
AppCompatEditText Phone = findViewById(R.id.phone);
Phone.addTextChangedListener(new TextWatcher() {
#Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
#Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
if (charSequence.length() == SpannedLength - chipLength)
{
SpannedLength = charSequence.length();
}
}
#Override
public void afterTextChanged(Editable editable) {
if(editable.length() - SpannedLength == chipLength) {
ChipDrawable chip = ChipDrawable.createFromResource(getContext(), R.xml.chip);
chip.setChipText(editable.subSequence(SpannedLength,editable.length()));
chip.setBounds(0, 0, chip.getIntrinsicWidth(), chip.getIntrinsicHeight());
ImageSpan span = new ImageSpan(chip);
editable.setSpan(span, SpannedLength, editable.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannedLength = editable.length();
}
}
});
Change chipLength according to your need when new chip need to be added in edittext.
OUTPUT
EDITED
You can find more about how to align center the text with span Here.
Here i added some code from the solution will fix for you..
public class VerticalImageSpan extends ImageSpan {
public VerticalImageSpan(Drawable drawable) {
super(drawable);
}
#Override
public int getSize(#NonNull Paint paint, CharSequence text, int start, int end,
Paint.FontMetricsInt fontMetricsInt) {
Drawable drawable = getDrawable();
Rect rect = drawable.getBounds();
if (fontMetricsInt != null) {
Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt();
int fontHeight = fmPaint.descent - fmPaint.ascent;
int drHeight = rect.bottom - rect.top;
int centerY = fmPaint.ascent + fontHeight / 2;
fontMetricsInt.ascent = centerY - drHeight / 2;
fontMetricsInt.top = fontMetricsInt.ascent;
fontMetricsInt.bottom = centerY + drHeight / 2;
fontMetricsInt.descent = fontMetricsInt.bottom;
}
return rect.right;
}
#Override
public void draw(#NonNull Canvas canvas, CharSequence text, int start, int end,
float x, int top, int y, int bottom, #NonNull Paint paint) {
Drawable drawable = getDrawable();
canvas.save();
Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt();
int fontHeight = fmPaint.descent - fmPaint.ascent;
int centerY = y + fmPaint.descent - fontHeight / 2;
int transY = centerY - (drawable.getBounds().bottom - drawable.getBounds().top) / 2;
canvas.translate(x, transY);
drawable.draw(canvas);
canvas.restore();
}
}
And change your imagespan class like below
VerticalImageSpan span = new VerticalImageSpan(chip);
All of the previous solutions didn't work for me if you want to achieve a gmail like behaviour with chips on multiple lines.
In order to do that I had to avoid using the ChipGroup and instead using a FlexboxLayout.
your_recipient_layout:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="#+id/recipient_label_TV"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_gravity="center_vertical" />
<com.google.android.flexbox.FlexboxLayout
android:id="#+id/recipient_group_FL"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_gravity="center_vertical"
app:flexWrap="wrap"
app:alignItems="stretch"
app:alignContent="space_around"
app:showDivider="beginning|middle|end"
app:dividerDrawable="#drawable/divider">
<EditText
android:id="#+id/recipient_input_ET"
android:layout_width="wrap_content"
android:layout_height="32dp"
app:layout_flexGrow="1"
android:background="#android:color/transparent"
android:imeOptions="actionDone"
android:inputType="text"/>
</com.google.android.flexbox.FlexboxLayout>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recipients_list_RV"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
The trick now is adding a new chip to the group but as second last position. Something like this:
private fun addNewChip(person: String, chipGroup: FlexboxLayout) {
val chip = Chip(context)
chip.text = person
chip.chipIcon = ContextCompat.getDrawable(requireContext(), R.mipmap.ic_launcher_round)
chip.isCloseIconEnabled = true
chip.isClickable = true
chip.isCheckable = false
chipGroup.addView(chip as View, chipGroup.childCount - 1)
chip.setOnCloseIconClickListener { chipGroup.removeView(chip as View) }
}
Using TextInputLayout
chip_item.xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.chip.Chip xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="wrap_content"
style="#style/Widget.MaterialComponents.Chip.Action"
app:closeIconEnabled="true"
android:layout_width="wrap_content" />
tags_input_layout.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="wrap_content">
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/i_input_v"
style="#style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:hint="Tags">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:gravity="bottom"
android:paddingBottom="22dp"
android:layout_height="match_parent" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.chip.ChipGroup
android:id="#+id/i_flex_box"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:alignContent="space_around"
app:alignItems="stretch"
app:flexWrap="wrap"
android:layout_marginTop="12dp"
android:layout_marginBottom="50dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:showDivider="beginning|middle|end">
</com.google.android.material.chip.ChipGroup>
</androidx.constraintlayout.widget.ConstraintLayout>
TagInputView.kt
class TagInputView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : ConstraintLayout(context, attrs) {
val chipGroup: ChipGroup
init {
LayoutInflater.from(context).inflate(R.layout.tags_input_layout, this, true)
val inputLayout = findViewById<TextInputLayout>(R.id.i_input_v)
val editText = inputLayout.editText!!
chipGroup = findViewById(R.id.i_flex_box)
editText.onFocusChangeListener = OnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
if (editText.text.toString() == " "){
editText.text.clear()
}
} else {
if (editText.text.isNullOrEmpty() && chipGroup.childCount > 0) {
editText.setText(" ")
}
}
}
editText.setOnKeyListener { _, _, event ->
if (event.action == KeyEvent.ACTION_DOWN) {
// onBackspacePressed, also edittext is empty
if (chipGroup.childCount <= 0) return#setOnKeyListener false
val lastChip = chipGroup.getChildAt(chipGroup.childCount - 1) as Chip
editText.append(lastChip.text)
chipGroup.removeView(lastChip)
}
false
}
editText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
val text = editable.toString()
if (text.isNotEmpty() && text.endsWith(",")) {
addNewChip(text.removeSuffix(","))
editable.clear()
}
}
})
}
private fun addNewChip(text: String) {
val newChip =
LayoutInflater.from(context).inflate(R.layout.chip_item, chipGroup, false) as Chip
newChip.id = ViewCompat.generateViewId()
newChip.text = text
newChip.setOnCloseIconClickListener {
chipGroup.removeView(newChip)
}
chipGroup.addView(newChip)
}
}
Usage
activity_main.xml
...
<com.shubhamgupta16.yourappid.utils.TagInputView
android:id="#+id/tagInputView"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
...
You can also access all Chip inside ChipGroup by using tagInputView.chipGroup property.
Output
We can do this by using material chips design itself without adding any extra styles.
Add it on app gradle For AndroidX
implementation 'com.google.android.material:material:1.0.0-beta01'
For earlier than AndroidX use this
implementation 'com.android.support:design:28.0.0'
Fragment
class EntryChipDemoFragment : Fragment() {
private lateinit var mView: View
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
mView = inflater.inflate(R.layout.fragment_entry_chip_demo, container, false)
mView.etValue.setOnEditorActionListener(TextView.OnEditorActionListener { v, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
val txtVal = v.text
if(!txtVal.isNullOrEmpty()) {
addChipToGroup(txtVal.toString(), mView.chipGroup2)
mView.etValue.setText("")
}
return#OnEditorActionListener true
}
false
})
return mView
}
private fun addChipToGroup(txt: String, chipGroup: ChipGroup) {
val chip = Chip(context)
chip.text = txt
// chip.chipIcon = ContextCompat.getDrawable(requireContext(), baseline_person_black_18)
chip.isCloseIconEnabled = true
chip.setChipIconTintResource(R.color.chipIconTint)
// necessary to get single selection working
chip.isClickable = false
chip.isCheckable = false
chipGroup.addView(chip as View)
chip.setOnCloseIconClickListener { chipGroup.removeView(chip as View) }
printChipsValue(chipGroup)
}
private fun printChipsValue(chipGroup: ChipGroup) {
for (i in 0 until chipGroup.childCount) {
val chipObj = chipGroup.getChildAt(i) as Chip
Log.d("Chips text :: " , chipObj.text.toString())
}
}
companion object {
#JvmStatic
fun newInstance() = EntryChipDemoFragment()
}
}
XML File:
<HorizontalScrollView
android:id="#+id/chipGroup2HorizontalView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:scrollbars="none"
app:layout_constraintVertical_bias="0.62">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Skills: " />
<com.google.android.material.chip.ChipGroup
android:id="#+id/chipGroup2"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:duplicateParentState="false">
</com.google.android.material.chip.ChipGroup>
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/textInputLayout"
android:layout_width="wrap_content"
android:layout_height="43dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="5dp"
android:minWidth="32dp"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#id/chipGroup2HorizontalView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_min="32dp">
<androidx.appcompat.widget.AppCompatEditText
android:id="#+id/etValue"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/transparent"
android:imeOptions="actionDone"
android:maxLines="1"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</HorizontalScrollView>
For more reference Click Here
You can use material chip "com.google.android.material.chip.Chip" and "implementation 'com.google.android.material:material:1.0.0' " add in build.gradle
Filter
style="#style/Widget.MaterialComponents.Chip.Filter"
Choice Chips
style="#style/Widget.MaterialComponents.Chip.Choice"
Entry input:
style="#style/Widget.MaterialComponents.Chip.Entry"
Usage
chipGroupView.attachTo(tagsInputEditText){ selection ->
// do something
}
Code
fun ChipGroup.attachTo(editText: EditText, chipChangedCallback: (List<String>) -> Unit) {
editText.apply {
onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
if (text.toString() == " ") {
text.clear()
}
} else {
if (text.isNullOrEmpty() && childCount > 0) {
setText(" ")
}
}
}
fun getSelection(): List<String> = mutableListOf<String>().apply {
for(view in children){
add(view.findViewById<Chip>(R.id.chipItem).text.toString())
}
}
fun checkHashtag(hashtagInput: String) {
if(Pattern.matches(Constants.TWITTER_HASHTAG_REGEX, hashtagInput)){
addTag(hashtagInput){
chipChangedCallback(getSelection())
}
text.clear()
chipChangedCallback(getSelection())
}else{
VibrateUtils.vibrate(200)
}
}
setOnKeyListener { _, actionId, event ->
if (event.action == KeyEvent.ACTION_DOWN && event.keyCode == KeyEvent.KEYCODE_DEL) {
if (text.isNotEmpty() || childCount <= 0) return#setOnKeyListener false
val lastChip = getChildAt(childCount - 1) as LinearLayout
editText.append(lastChip.findViewById<Chip>(R.id.chipItem).text)
removeView(lastChip)
chipChangedCallback(getSelection())
}
else if(
actionId == EditorInfo.IME_ACTION_SEARCH
|| actionId == EditorInfo.IME_ACTION_DONE
|| event.action == KeyEvent.ACTION_DOWN
&& event.keyCode == KeyEvent.KEYCODE_ENTER){
checkHashtag(text.toString())
return#setOnKeyListener true
}
actionId == KeyEvent.KEYCODE_ENTER
}
addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
val text = editable.toString()
val length = text.length
if(text == " "){
editable.delete(length - 1, length)
return
}
if (text.isNotEmpty() && text.endsWith(" ")) {
val hashtagInput = text.removeSuffix(" ")
checkHashtag(hashtagInput)
}
}
})
}
}
private fun ChipGroup.addTag(tag: String,onRemoved: (View)-> Unit = {}) {
addView(ChipBinding.inflate(LayoutInflater.from(context)).also { itemUi ->
itemUi.root.findViewById<Chip>(R.id.chipItem).apply {
text = tag
setOnClickListener {
this#addTag.removeView(itemUi.root);
onRemoved(this)
}
}
}.root)
}
chip.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools">
<com.google.android.material.chip.Chip
android:id="#+id/chipItem"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="wrap_content"
app:chipIconSize="16dp"
app:iconStartPadding="6dp"
tools:text="Non-Fiction"
app:chipIcon="#drawable/ic_hash"
style="#style/Widget.MaterialComponents.Chip.Action"
app:closeIconEnabled="true"
android:layout_width="wrap_content" />
</LinearLayout>
Note: chipGroupView and tagsInputEditText are vertically wrapped in LinearLayout with a stroked background
Not the ideal solution, but definitely easy
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.chip.Chip
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:chipMinTouchTargetSize="0dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="10">
<EditText
android:id="#+id/add_frag_label_et_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="8dp"
android:layout_weight="9"
android:background="#null"
android:hint="Enter Label"
android:inputType="text"
android:paddingStart="8dp"
android:paddingEnd="0dp"
android:textAppearance="#style/TextAppearance.AppCompat.Medium"
android:textColor="#color/black"
android:textSize="14sp" />
<ImageView
android:id="#+id/add_frag_label_iv_add"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center"
app:tint="#6b6a6c"
android:layout_weight="1"
android:contentDescription="Add Label to note."
android:src="#mipmap/ic_plus_foreground" />
</LinearLayout>
</FrameLayout>

Categories

Resources