I am using CameraX(QuickStart CameraX Basic from GitHub) and firebase ML kit for live face detection. Everything works fine except bounding box is not being drawn on the face but away from the face.
I am using ML kit quickStart GraphicOverlay.Class and FaceGraphic.class.
GraphicOverlay.java
public class GraphicOverlay extends View {
private final Object mLock = new Object();
private int mPreviewWidth;
private float mWidthScaleFactor = 1.5f;
private int mPreviewHeight;
private float mHeightScaleFactor = 1.5f;
private int mFacing = CameraSource.CAMERA_FACING_BACK;
private Set<Graphic> mGraphics = new HashSet<>();
public GraphicOverlay(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void clear() {
synchronized (mLock) {
mGraphics.clear();
}
postInvalidate();
}
public void add(Graphic graphic) {
synchronized (mLock) {
mGraphics.add(graphic);
}
postInvalidate();
}
public void remove(Graphic graphic) {
synchronized (mLock) {
mGraphics.remove(graphic);
}
postInvalidate();
}
public void setCameraInfo(int previewWidth, int previewHeight, int facing) {
synchronized (mLock) {
mPreviewWidth = previewWidth;
mPreviewHeight = previewHeight;
mFacing = facing;
}
postInvalidate();
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
synchronized (mLock) {
if ((mPreviewWidth != 0) && (mPreviewHeight != 0)) {
mWidthScaleFactor = (float) getWidth() / (float) mPreviewWidth;
mHeightScaleFactor = (float)getHeight() / (float) mPreviewHeight;
}
for (Graphic graphic : mGraphics) {
graphic.draw(canvas);
}
}
}
public static abstract class Graphic {
private GraphicOverlay mOverlay;
public Graphic(GraphicOverlay overlay) {
mOverlay = overlay;
}
public abstract void draw(Canvas canvas);
public float scaleX(float horizontal) {
return horizontal * mOverlay.mWidthScaleFactor;
}
public float scaleY(float vertical) {
return vertical * mOverlay.mHeightScaleFactor;
}
public float translateX(float x) {
if (mOverlay.mFacing == CameraSource.CAMERA_FACING_FRONT) {
return mOverlay.getWidth() - scaleX(x);
} else {
return scaleX(x);
}
}
public float translateY(float y) {
return scaleY(y);
}
public void postInvalidate() {
mOverlay.postInvalidate();
}
}
}
FaceGraphic.java
public class FaceGraphic extends GraphicOverlay.Graphic {
private static final float FACE_POSITION_RADIUS = 10.0f;
private static final float ID_TEXT_SIZE = 40.0f;
private static final float ID_Y_OFFSET = 50.0f;
private static final float ID_X_OFFSET = -50.0f;
private static final float BOX_STROKE_WIDTH = 5.0f;
private static final int[] COLOR_CHOICES = {
Color.BLUE //, Color.CYAN, Color.GREEN, Color.MAGENTA, Color.RED, Color.WHITE, Color.YELLOW
};
private static int currentColorIndex = 0;
private final Paint facePositionPaint;
private final Paint idPaint, centerPoint;
private final Paint boxPaint, screenCenterPaint;
private final Paint movePaint;
GraphicOverlay graphicOverlay;
private int facing;
private volatile FirebaseVisionFace firebaseVisionFace;
public FaceGraphic(GraphicOverlay overlay) {
super(overlay);
this.graphicOverlay = overlay;
currentColorIndex = (currentColorIndex + 1) % COLOR_CHOICES.length;
final int selectedColor = COLOR_CHOICES[currentColorIndex];
screenCenterPaint = new Paint();
screenCenterPaint.setColor(Color.GREEN);
facePositionPaint = new Paint();
facePositionPaint.setColor(selectedColor);
idPaint = new Paint();
idPaint.setColor(Color.WHITE);
idPaint.setTextSize(ID_TEXT_SIZE);
boxPaint = new Paint();
boxPaint.setColor(Color.WHITE);
boxPaint.setStyle(Paint.Style.STROKE);
boxPaint.setStrokeWidth(BOX_STROKE_WIDTH);
centerPoint = new Paint();
centerPoint.setStrokeWidth(5f);
centerPoint.setColor(Color.RED);
centerPoint.setStyle(Paint.Style.STROKE);
movePaint = new Paint();
movePaint.setColor(Color.RED);
movePaint.setTextSize(38);
}
public void updateFace(FirebaseVisionFace face, int facing) {
firebaseVisionFace = face;
this.facing = facing;
postInvalidate();
}
#Override
public void draw(Canvas canvas) {
canvas.drawCircle(canvas.getWidth() / 2, canvas.getHeight() / 2, 10, screenCenterPaint);
FirebaseVisionFace face = firebaseVisionFace;
if (face == null) {
return;
}
// Draws a circle at the position of the detected face, with the face's track id below.
float x = translateX(face.getBoundingBox().centerX());
float y = translateY(face.getBoundingBox().centerY());
canvas.drawCircle(x, y, FACE_POSITION_RADIUS, facePositionPaint);
Log.d("myFaceBounds", String.valueOf(face.getBoundingBox()));
float faceRightOrLeftAngle = face.getHeadEulerAngleY();
float faceTiltAngle = face.getHeadEulerAngleZ();
// Draws a bounding box around the face.
float xOffset = scaleX(face.getBoundingBox().width() / 2.0f);
float yOffset = scaleY(face.getBoundingBox().height() / 2.0f);
float left = x - xOffset - 100;
float top = y - yOffset - 100;
float right = x + xOffset + 100;
float bottom = y + yOffset + 100;
canvas.drawRect(left, top, right, bottom, boxPaint);
}
}
CameraFragment.kt
class CameraFragment : Fragment() {
private lateinit var container: FrameLayout
private lateinit var viewFinder: TextureView
private lateinit var outputDirectory: File
private lateinit var broadcastManager: LocalBroadcastManager
private var displayId = -1
private var lensFacing = CameraX.LensFacing.BACK
private var preview: Preview? = null
private var imageCapture: ImageCapture? = null
private var imageAnalyzer: ImageAnalysis? = null
private lateinit var graphicOverlay: GraphicOverlay
private val volumeDownReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val keyCode = intent.getIntExtra(KEY_EVENT_EXTRA, KeyEvent.KEYCODE_UNKNOWN)
when (keyCode) {
// When the volume down button is pressed, simulate a shutter button click
KeyEvent.KEYCODE_VOLUME_DOWN -> {
val shutter = container
.findViewById<ImageButton>(R.id.camera_capture_button)
shutter.simulateClick()
}
}
}
}
private val analyzerThread = HandlerThread("LuminosityAnalysis").apply { start() }
private lateinit var displayManager: DisplayManager
private val displayListener = object : DisplayManager.DisplayListener {
override fun onDisplayAdded(displayId: Int) = Unit
override fun onDisplayRemoved(displayId: Int) = Unit
override fun onDisplayChanged(displayId: Int) = view?.let { view ->
if (displayId == this#CameraFragment.displayId) {
Log.d(TAG, "Rotation changed: ${view.display.rotation}")
preview?.setTargetRotation(view.display.rotation)
imageCapture?.setTargetRotation(view.display.rotation)
imageAnalyzer?.setTargetRotation(view.display.rotation)
}
} ?: Unit
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
}
override fun onResume() {
super.onResume()
if (!PermissionsFragment.hasPermissions(requireContext())) {
Navigation.findNavController(requireActivity(), R.id.fragment_container).navigate(
CameraFragmentDirections.actionCameraToPermissions())
}
}
override fun onDestroyView() {
super.onDestroyView()
broadcastManager.unregisterReceiver(volumeDownReceiver)
displayManager.unregisterDisplayListener(displayListener)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.fragment_camera, container, false)
private fun setGalleryThumbnail(file: File) {
val thumbnail = container.findViewById<ImageButton>(R.id.photo_view_button)
thumbnail.post {
thumbnail.setPadding(resources.getDimension(R.dimen.stroke_small).toInt())
Glide.with(thumbnail)
.load(file)
.apply(RequestOptions.circleCropTransform())
.into(thumbnail)
}
}
private val imageSavedListener = object : ImageCapture.OnImageSavedListener {
override fun onError(
error: ImageCapture.UseCaseError, message: String, exc: Throwable?) {
Log.e(TAG, "Photo capture failed: $message")
exc?.printStackTrace()
}
override fun onImageSaved(photoFile: File) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setGalleryThumbnail(photoFile)
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
requireActivity().sendBroadcast(
Intent(Camera.ACTION_NEW_PICTURE, Uri.fromFile(photoFile)))
}
val mimeType = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(photoFile.extension)
MediaScannerConnection.scanFile(
context, arrayOf(photoFile.absolutePath), arrayOf(mimeType), null)
}
}
/*---------------------------------------------------------------------------------------------------------------------------------------------*/
#SuppressLint("MissingPermission")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
container = view as FrameLayout
viewFinder = container.findViewById(R.id.view_finder)
graphicOverlay = container.findViewById(R.id.graphicOverlay)
broadcastManager = LocalBroadcastManager.getInstance(view.context)
val filter = IntentFilter().apply { addAction(KEY_EVENT_ACTION) }
broadcastManager.registerReceiver(volumeDownReceiver, filter)
displayManager = viewFinder.context
.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
displayManager.registerDisplayListener(displayListener, null)
outputDirectory = MainActivity.getOutputDirectory(requireContext())
viewFinder.post {
displayId = viewFinder.display.displayId
updateCameraUi()
bindCameraUseCases()
lifecycleScope.launch(Dispatchers.IO) {
outputDirectory.listFiles { file ->
EXTENSION_WHITELIST.contains(file.extension.toUpperCase())
}.sorted().reversed().firstOrNull()?.let { setGalleryThumbnail(it) }
}
}
}
private fun bindCameraUseCases() {
val metrics = DisplayMetrics().also { viewFinder.display.getRealMetrics(it) }
val screenAspectRatio = Rational(metrics.widthPixels, metrics.heightPixels)
Log.d(TAG, "Screen metrics: ${metrics.widthPixels} x ${metrics.heightPixels}")
val viewFinderConfig = PreviewConfig.Builder().apply {
setLensFacing(lensFacing)
setTargetAspectRatio(screenAspectRatio)
setTargetRotation(viewFinder.display.rotation)
}.build()
preview = AutoFitPreviewBuilder.build(viewFinderConfig, viewFinder)
graphicOverlay.setCameraInfo(metrics.widthPixels, metrics.heightPixels, getLensFacing())
val imageCaptureConfig = ImageCaptureConfig.Builder().apply {
setLensFacing(lensFacing)
setCaptureMode(CaptureMode.MIN_LATENCY)
setTargetAspectRatio(screenAspectRatio)
setTargetRotation(viewFinder.display.rotation)
}.build()
imageCapture = ImageCapture(imageCaptureConfig)
// Setup image analysis pipeline that computes average pixel luminance in real time
val analyzerConfig = ImageAnalysisConfig.Builder().apply {
setLensFacing(lensFacing)
setCallbackHandler(Handler(analyzerThread.looper))
setImageReaderMode(ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE)
setTargetRotation(viewFinder.display.rotation)
}.build()
imageAnalyzer = ImageAnalysis(analyzerConfig).apply {
analyzer = LuminosityAnalyzer(graphicOverlay)
}
CameraX.bindToLifecycle(
viewLifecycleOwner, preview, imageCapture, imageAnalyzer)
}
#SuppressLint("RestrictedApi")
private fun updateCameraUi() {
container.findViewById<ConstraintLayout>(R.id.camera_ui_container)?.let {
container.removeView(it)
}
val controls = View.inflate(requireContext(), R.layout.camera_ui_container, container)
controls.findViewById<ImageButton>(R.id.camera_capture_button).setOnClickListener {
imageCapture?.let { imageCapture ->
val photoFile = createFile(outputDirectory, FILENAME, PHOTO_EXTENSION)
val metadata = Metadata().apply {
isReversedHorizontal = lensFacing == CameraX.LensFacing.FRONT
}
imageCapture.takePicture(photoFile, imageSavedListener, metadata)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
container.postDelayed({
container.foreground = ColorDrawable(Color.WHITE)
container.postDelayed(
{ container.foreground = null }, ANIMATION_FAST_MILLIS)
}, ANIMATION_SLOW_MILLIS)
}
}
}
controls.findViewById<ImageButton>(R.id.camera_switch_button).setOnClickListener {
lensFacing = if (CameraX.LensFacing.FRONT == lensFacing) {
CameraX.LensFacing.BACK
} else {
CameraX.LensFacing.FRONT
}
try {
CameraX.getCameraWithLensFacing(lensFacing)
CameraX.unbindAll()
bindCameraUseCases()
} catch (exc: Exception) {
}
}
controls.findViewById<ImageButton>(R.id.photo_view_button).setOnClickListener {
Navigation.findNavController(requireActivity(), R.id.fragment_container).navigate(
CameraFragmentDirections.actionCameraToGallery(outputDirectory.absolutePath))
}
}
private class LuminosityAnalyzer(graphicOverlay: GraphicOverlay) : ImageAnalysis.Analyzer {
val graphicOverlay = graphicOverlay
private var lastAnalyzedTimestamp = 0L
private fun getRotation(rotationCompensation: Int): Int {
val result: Int
when (rotationCompensation) {
0 -> result = FirebaseVisionImageMetadata.ROTATION_0
90 -> result = FirebaseVisionImageMetadata.ROTATION_90
180 -> result = FirebaseVisionImageMetadata.ROTATION_180
270 -> result = FirebaseVisionImageMetadata.ROTATION_270
else -> {
result = FirebaseVisionImageMetadata.ROTATION_0
}
}
return result
}
override fun analyze(image: ImageProxy, rotationDegrees: Int) {
val currentTimestamp = System.currentTimeMillis()
if (currentTimestamp - lastAnalyzedTimestamp >= TimeUnit.MILLISECONDS.toMillis(300)) {
lastAnalyzedTimestamp = currentTimestamp
try {
val y = image.planes[0]
val u = image.planes[1]
val v = image.planes[2]
//Then we can then get the number of pixels in each plane
val Yb = y.buffer.remaining()
val Ub = u.buffer.remaining()
val Vb = v.buffer.remaining()
//and convert them into a single YUV formatted ByteArray
val data = ByteArray(Yb + Ub + Vb)
y.buffer.get(data, 0, Yb)
u.buffer.get(data, Yb, Ub)
v.buffer.get(data, Yb + Ub, Vb)
val metadata = FirebaseVisionImageMetadata.Builder()
.setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_YV12)
.setHeight(image.height)
.setWidth(image.width)
.setRotation(getRotation(rotationDegrees))
.build()
val options = FirebaseVisionFaceDetectorOptions.Builder()
.setPerformanceMode(FirebaseVisionFaceDetectorOptions.FAST)
.setLandmarkMode(FirebaseVisionFaceDetectorOptions.ALL_LANDMARKS)
.enableTracking()
.build()
val labelImage = FirebaseVisionImage.fromByteArray(data, metadata)
val detector = FirebaseVision.getInstance().getVisionFaceDetector(options)
detector.detectInImage(labelImage)
.addOnSuccessListener { faces ->
graphicOverlay.clear()
for (face in faces) {
val faceGraphic = FaceGraphic(graphicOverlay)
graphicOverlay.add(faceGraphic)
faceGraphic.updateFace(face, 1)
}
}
.addOnFailureListener { }
} catch (e: IllegalStateException) {
}
}
}
}
companion object {
private const val TAG = "CameraXBasic"
private const val FILENAME = "yyyy-MM-dd-HH-mm-ss-SSS"
private const val PHOTO_EXTENSION = ".jpg"
private fun createFile(baseFolder: File, format: String, extension: String) =
File(baseFolder, SimpleDateFormat(format, Locale.US)
.format(System.currentTimeMillis()) + extension)
}
private fun getLensFacing(): Int {
return if (lensFacing == CameraX.LensFacing.BACK) {
0
} else
1
}
}
AutoFitPreviewBuilder.kt
class AutoFitPreviewBuilder private constructor(
config: PreviewConfig, viewFinderRef: WeakReference<TextureView>) {
/** Public instance of preview use-case which can be used by consumers of this adapter */
val useCase: Preview
/** Internal variable used to keep track of the use case's output rotation */
private var bufferRotation: Int = 0
/** Internal variable used to keep track of the view's rotation */
private var viewFinderRotation: Int? = null
/** Internal variable used to keep track of the use-case's output dimension */
private var bufferDimens: Size = Size(0, 0)
/** Internal variable used to keep track of the view's dimension */
private var viewFinderDimens: Size = Size(0, 0)
/** Internal variable used to keep track of the view's display */
private var viewFinderDisplay: Int = -1
/** Internal reference of the [DisplayManager] */
private lateinit var displayManager: DisplayManager
/**
* We need a display listener for orientation changes that do not trigger a configuration
* change, for example if we choose to override config change in manifest or for 180-degree
* orientation changes.
*/
private val displayListener = object : DisplayManager.DisplayListener {
override fun onDisplayAdded(displayId: Int) = Unit
override fun onDisplayRemoved(displayId: Int) = Unit
override fun onDisplayChanged(displayId: Int) {
val viewFinder = viewFinderRef.get() ?: return
if (displayId == viewFinderDisplay) {
val display = displayManager.getDisplay(displayId)
val rotation = getDisplaySurfaceRotation(display)
updateTransform(viewFinder, rotation, bufferDimens, viewFinderDimens)
}
}
}
init {
// Make sure that the view finder reference is valid
val viewFinder = viewFinderRef.get() ?:
throw IllegalArgumentException("Invalid reference to view finder used")
// Initialize the display and rotation from texture view information
viewFinderDisplay = viewFinder.display.displayId
viewFinderRotation = getDisplaySurfaceRotation(viewFinder.display) ?: 0
// Initialize public use-case with the given config
useCase = Preview(config)
// Every time the view finder is updated, recompute layout
useCase.onPreviewOutputUpdateListener = Preview.OnPreviewOutputUpdateListener {
val viewFinder =
viewFinderRef.get() ?: return#OnPreviewOutputUpdateListener
Log.d(TAG, "Preview output changed. " +
"Size: ${it.textureSize}. Rotation: ${it.rotationDegrees}")
// To update the SurfaceTexture, we have to remove it and re-add it
val parent = viewFinder.parent as ViewGroup
parent.removeView(viewFinder)
parent.addView(viewFinder, 0)
// Update internal texture
viewFinder.surfaceTexture = it.surfaceTexture
// Apply relevant transformations
bufferRotation = it.rotationDegrees
val rotation = getDisplaySurfaceRotation(viewFinder.display)
updateTransform(viewFinder, rotation, it.textureSize, viewFinderDimens)
}
// Every time the provided texture view changes, recompute layout
viewFinder.addOnLayoutChangeListener { view, left, top, right, bottom, _, _, _, _ ->
val viewFinder = view as TextureView
val newViewFinderDimens = Size(right - left, bottom - top)
Log.d(TAG, "View finder layout changed. Size: $newViewFinderDimens")
val rotation = getDisplaySurfaceRotation(viewFinder.display)
updateTransform(viewFinder, rotation, bufferDimens, newViewFinderDimens)
}
// Every time the orientation of device changes, recompute layout
// NOTE: This is unnecessary if we listen to display orientation changes in the camera
// fragment and call [Preview.setTargetRotation()] (like we do in this sample), which will
// trigger [Preview.OnPreviewOutputUpdateListener] with a new
// [PreviewOutput.rotationDegrees]. CameraX Preview use case will not rotate the frames for
// us, it will just tell us about the buffer rotation with respect to sensor orientation.
// In this sample, we ignore the buffer rotation and instead look at the view finder's
// rotation every time [updateTransform] is called, which gets triggered by
// [CameraFragment] display listener -- but the approach taken in this sample is not the
// only valid one.
displayManager = viewFinder.context
.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
displayManager.registerDisplayListener(displayListener, null)
// Remove the display listeners when the view is detached to avoid holding a reference to
// it outside of the Fragment that owns the view.
// NOTE: Even though using a weak reference should take care of this, we still try to avoid
// unnecessary calls to the listener this way.
viewFinder.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(view: View?) =
displayManager.registerDisplayListener(displayListener, null)
override fun onViewDetachedFromWindow(view: View?) =
displayManager.unregisterDisplayListener(displayListener)
})
}
/** Helper function that fits a camera preview into the given [TextureView] */
private fun updateTransform(textureView: TextureView?, rotation: Int?, newBufferDimens: Size,
newViewFinderDimens: Size) {
// This should not happen anyway, but now the linter knows
val textureView = textureView ?: return
if (rotation == viewFinderRotation &&
Objects.equals(newBufferDimens, bufferDimens) &&
Objects.equals(newViewFinderDimens, viewFinderDimens)) {
// Nothing has changed, no need to transform output again
return
}
if (rotation == null) {
// Invalid rotation - wait for valid inputs before setting matrix
return
} else {
// Update internal field with new inputs
viewFinderRotation = rotation
}
if (newBufferDimens.width == 0 || newBufferDimens.height == 0) {
// Invalid buffer dimens - wait for valid inputs before setting matrix
return
} else {
// Update internal field with new inputs
bufferDimens = newBufferDimens
}
if (newViewFinderDimens.width == 0 || newViewFinderDimens.height == 0) {
// Invalid view finder dimens - wait for valid inputs before setting matrix
return
} else {
// Update internal field with new inputs
viewFinderDimens = newViewFinderDimens
}
val matrix = Matrix()
Log.d(TAG, "Applying output transformation.\n" +
"View finder size: $viewFinderDimens.\n" +
"Preview output size: $bufferDimens\n" +
"View finder rotation: $viewFinderRotation\n" +
"Preview output rotation: $bufferRotation")
// Compute the center of the view finder
val centerX = viewFinderDimens.width / 2f
val centerY = viewFinderDimens.height / 2f
// Correct preview output to account for display rotation
matrix.postRotate(-viewFinderRotation!!.toFloat(), centerX, centerY)
// Buffers are rotated relative to the device's 'natural' orientation: swap width and height
val bufferRatio = bufferDimens.height / bufferDimens.width.toFloat()
val scaledWidth: Int
val scaledHeight: Int
// Match longest sides together -- i.e. apply center-crop transformation
if (viewFinderDimens.width > viewFinderDimens.height) {
scaledHeight = viewFinderDimens.width
scaledWidth = Math.round(viewFinderDimens.width * bufferRatio)
} else {
scaledHeight = viewFinderDimens.height
scaledWidth = Math.round(viewFinderDimens.height * bufferRatio)
}
// Compute the relative scale value
val xScale = scaledWidth / viewFinderDimens.width.toFloat()
val yScale = scaledHeight / viewFinderDimens.height.toFloat()
// Scale input buffers to fill the view finder
matrix.preScale(xScale, yScale, centerX, centerY)
// Finally, apply transformations to our TextureView
textureView.setTransform(matrix)
}
companion object {
private val TAG = AutoFitPreviewBuilder::class.java.simpleName
/** Helper function that gets the rotation of a [Display] in degrees */
fun getDisplaySurfaceRotation(display: Display?) = when(display?.rotation) {
Surface.ROTATION_0 -> 0
Surface.ROTATION_90 -> 90
Surface.ROTATION_180 -> 180
Surface.ROTATION_270 -> 270
else -> null
}
/**
* Main entrypoint for users of this class: instantiates the adapter and returns an instance
* of [Preview] which automatically adjusts in size and rotation to compensate for
* config changes.
*/
fun build(config: PreviewConfig, viewFinder: TextureView) =
AutoFitPreviewBuilder(config, WeakReference(viewFinder)).useCase
}
}
Related
Custom ItemDecoration:
class DemoDecoration(context: Context, left: Float, top: Float, right: Float, bottom: Float) : ItemDecoration() {
companion object {
private const val dividerHeight = 8f
private const val dividerPaddingLeft = 0f
}
private val left: Int
private val top: Int
private val right: Int
private val bottom: Int
private val dividerPaddingRight = 0f
private val dividerPaint: Paint
private val textPaint: Paint
init {
val density = context.resources.displayMetrics.density
this.left = (density * left).toInt()
this.top = (density * top).toInt()
this.right = (density * right).toInt()
this.bottom = (density * bottom).toInt()
dividerPaint = Paint()
dividerPaint.isAntiAlias = true
dividerPaint.style = Paint.Style.FILL
dividerPaint.color = ContextCompat.getColor(context, android.R.color.darker_gray)
textPaint = Paint()
textPaint.isAntiAlias = true
textPaint.style = Paint.Style.FILL
textPaint.textSize = 30f
textPaint.typeface = Typeface.DEFAULT_BOLD
textPaint.color = Color.BLACK
}
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
draw(canvas, parent)
}
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
draw(canvas, parent)
}
private fun draw(canvas: Canvas, parent: RecyclerView) {
if (parent.layoutManager == null) {
return
}
canvas.save()
drawRect(canvas, parent)
canvas.restore()
}
private fun drawRect(canvas: Canvas, parent: RecyclerView) {
val childCount = parent.childCount
val spanCount = getSpanCount(parent)
val space = 0f.coerceAtLeast(((top + bottom).toFloat() - dividerHeight) / 2)
var i = 0
if (i < childCount) {
val view = parent.getChildAt(i)
val position = parent.getChildAdapterPosition(view)
if (position / spanCount % 2 == 0) {
canvas.drawRect(dividerPaddingLeft, view.bottom + space,
parent.right - dividerPaddingRight, view.bottom + space + dividerHeight,
dividerPaint)
}
i += spanCount
}
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
if (parent.layoutManager == null) {
return
}
val position = parent.getChildAdapterPosition(view)
val spanCount = getSpanCount(parent)
val index = position / spanCount
if (index > 0 && index % 2 == 0) {
outRect[left, 56, right] = bottom
} else {
outRect[left, top, right] = bottom
}
}
private fun getSpanCount(parent: RecyclerView): Int {
var spanCount = -1
val layoutManager = parent.layoutManager
if (layoutManager is GridLayoutManager) {
spanCount = layoutManager.spanCount
} else if (layoutManager is StaggeredGridLayoutManager) {
spanCount = layoutManager.spanCount
}
return spanCount
}
}
change item position
class MainActivity: AppCompatActivity() {
private lateinit var change: Button
private lateinit var recycler: RecyclerView
private val demoAdapter: DemoAdapter = DemoAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
change = findViewById(R.id.change)
change.setOnClickListener {
val list = ArrayList(demoAdapter.currentList)
list.add(10, list.removeAt(5))
demoAdapter.submitList(list)
}
recycler = findViewById(R.id.recycler)
recycler.apply {
layoutManager = GridLayoutManager(this#MainActivity, 5)
setHasFixedSize(true)
isNestedScrollingEnabled = false
itemAnimator = DefaultItemAnimator()
addItemDecoration(DemoDecoration(this#MainActivity, 2f, 4f, 2f, 4f))
adapter = demoAdapter
}
val data: MutableList<Model> = ArrayList()
for (i: Int in 0 .. 39) {
when ((1..2).random()) {
1 -> data.add(Model("$i\n\na"))
2 -> data.add(Model("$i\na\nb"))
}
}
demoAdapter.submitList(data)
}
}
adapter
class DemoAdapter: ListAdapter<Model, DemoAdapter.ViewHolder>(TaskDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val model = getItem(position)
holder.text.text = model.text
}
class ViewHolder(view: View): RecyclerView.ViewHolder(view) {
val container: ConstraintLayout
val text: TextView
init {
container = view.findViewById(R.id.container)
text = view.findViewById(R.id.text)
}
}
private class TaskDiffCallback : DiffUtil.ItemCallback<Model>() {
override fun areItemsTheSame(oldItem: Model, newItem: Model): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Model, newItem: Model): Boolean {
return oldItem == newItem
}
}
}
data class Model(val text: String)
item 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:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#drawable/shape_stroke_black">
<TextView
android:id="#+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_gravity="center"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:background="#android:color/holo_blue_light"
android:textColor="#android:color/black"
android:textSize="12sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
RecyclerviewDemo
5-->10
Item height is changed. How can to keep item height unchanged?
You can fix the size of the items you create for the recyclerview. Try to give the average size of the incoming data as width height in the design. or in html logic : width: 100vw;
height: 100vh; can you try
Hello I am New to Programming i just created an application that get the data from server every second i just wants to changethe background of my RecyclerViews holder's bid and ask position if the data if grater or lower then previous.
for example my init data at bid price is 4000 after one second if my bid price of the item is increase and then the position at recyclerview's change the background color of it.
i just implement this code but its randomly changing the background also when the price are not changed.
class Adapter(private val product: ArrayList<Products>) :
RecyclerView.Adapter<McxAdapter.CustomViewHolder>() {
var olddatabid: ArrayList<String> = ArrayList()
var newdatabid: ArrayList<String> = ArrayList()
var olddataask: ArrayList<String> = ArrayList()
var newdataask: ArrayList<String> = ArrayList()
fun addNewStatutes(items: ArrayList<Products>) {
product.clear()
this.product.addAll(items)
if (product.isNotEmpty()) {
notifyDataSetChanged()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {
val itemView = LayoutInflater.from(parent.context)
.inflate(R.layout.item_mcx, parent, false)
return CustomViewHolder(itemView)
}
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
try {
val datum = product[position]
holder.txtSymbol.text = datum.symbol
holder.txtdate.text = datum.serExp
holder.txtBuy.text = datum.buyPrice
holder.txtSell.text = datum.sellPrice
holder.txtLtp.text = datum.lastTradedPrice
holder.txtHigh.text = datum.high
holder.txtLow.text = datum.low
holder.txtOpen.text = datum.open
holder.txtClose.text = datum.close
holder.txtChange.text = datum.netChangeInRs
if (newdatabid.size < product.size) {
newdatabid.add(datum.buyPrice.toString())
}
if (olddatabid.size < product.size) {
olddatabid.add(datum.buyPrice.toString())
}
if (newdataask.size < product.size) {
newdataask.add(datum.sellPrice.toString())
}
if (olddataask.size < product.size) {
olddataask.add(datum.sellPrice.toString())
}
newdatabid[position] = datum.buyPrice.toString()
newdataask[position] = datum.sellPrice.toString()
if (newdatabid[position].toFloat() > olddatabid[position].toFloat()) {
holder.txtBuy.setBackgroundColor(Color.BLUE)
}
if (newdatabid[position].toFloat() < olddatabid[position].toFloat()) {
holder.txtBuy.setBackgroundColor(Color.RED)
}
if (newdataask[position].toFloat() > olddataask[position].toFloat()) {
holder.txtSell.setBackgroundColor(Color.BLUE)
}
if (newdataask[position].toFloat() < olddataask[position].toFloat()) {
holder.txtSell.setBackgroundColor(Color.RED)
}
olddatabid[position] = newdatabid[position]
olddataask[position] = newdataask[position]
} catch (e: Exception) {
}
}
override fun getItemCount(): Int {
return if (product.size > 0 && product.isNotEmpty()) {
product.size
} else {
0
}
}
inner class CustomViewHolder(view: View) : RecyclerView.ViewHolder(view) {
internal var txtSymbol: TextView = itemView.findViewById(R.id.scriptname)
internal var txtdate: TextView = itemView.findViewById(R.id.date)
internal var txtBuy: TextView = itemView.findViewById(R.id.buy)
internal var txtSell: TextView = itemView.findViewById(R.id.sell)
internal var txtLtp: TextView = itemView.findViewById(R.id.currentvalue)
internal var txtHigh: TextView = itemView.findViewById(R.id.high)
internal var txtLow: TextView = itemView.findViewById(R.id.low)
internal var txtOpen: TextView = itemView.findViewById(R.id.open)
internal var txtClose: TextView = itemView.findViewById(R.id.close)
internal var txtChange: TextView = itemView.findViewById(R.id.change)
internal var txtRupp: TextView = itemView.findViewById(R.id.rupp)
}
}
Try to remove if statements in if else or when statement, then check the results
if (newdatabid.size < product.size) {
newdatabid.add(datum.buyPrice.toString())
}
if (olddatabid.size < product.size) {
olddatabid.add(datum.buyPrice.toString())
}
if (newdataask.size < product.size) {
newdataask.add(datum.sellPrice.toString())
}
if (olddataask.size < product.size) {
olddataask.add(datum.sellPrice.toString())
}
Make your class a data class and use AsyncListDiffer.submitList to update adapter
Here is a good article about implementing diffutil https://medium.com/#iammert/using-diffutil-in-android-recyclerview-bdca8e4fbb00
I have a TabLayout with Recyclerview so that when tabs are clicked then the Recyclerview is scrolled to a particular position.
I want the reverse procedure as well such that when the Recyclerview is scrolled to a particular position then the particular tab is highlighted.
For example: If there are 4 tabs in the TabLayout and when Recyclerview is scrolled to 5th position (item visible and below TabLayout) then 3rd tab should be highlighted.
Here when 'How it works' appears below TabLayout then tabs 'How it works' should be highlighted.
Try this
follow this steps
Add a ScrollListener to your RecyclerView
than find first visible item of your RecyclerView
set the select the tab in TabLayout as per position of your RecyclerView
SAMPLE CODE
myRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int itemPosition=linearLayoutManager.findFirstCompletelyVisibleItemPosition();
if(itemPosition==0){ // item position of uses
TabLayout.Tab tab = myTabLayout.getTabAt(Index);
tab.select();
}else if(itemPosition==1){// item position of side effects
TabLayout.Tab tab = myTabLayout.getTabAt(Index);
tab.select();
}else if(itemPosition==2){// item position of how it works
TabLayout.Tab tab = myTabLayout.getTabAt(Index);
tab.select();
}else if(itemPosition==3){// item position of precaution
TabLayout.Tab tab = myTabLayout.getTabAt(Index);
tab.select();
}
}
});
EDIT
public class MyActivity extends AppCompatActivity {
RecyclerView myRecyclerView;
TabLayout myTabLayout;
LinearLayoutManager linearLayoutManager;
ArrayList<String> arrayList = new ArrayList<>();
DataAdapter adapter;
private boolean isUserScrolling = false;
private boolean isListGoingUp = true;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);
myTabLayout = findViewById(R.id.myTabLayout);
myRecyclerView = findViewById(R.id.myRecyclerView);
linearLayoutManager = new LinearLayoutManager(this);
myRecyclerView.setLayoutManager(linearLayoutManager);
myRecyclerView.setHasFixedSize(true);
for (int i = 0; i < 120; i++) {
arrayList.add("Item " + i);
}
adapter= new DataAdapter(this,arrayList);
myRecyclerView.setAdapter(adapter);
myTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
#Override
public void onTabSelected(TabLayout.Tab tab) {
isUserScrolling = false ;
int position = tab.getPosition();
if(position==0){
myRecyclerView.smoothScrollToPosition(0);
}else if(position==1){
myRecyclerView.smoothScrollToPosition(30);
}else if(position==2){
myRecyclerView.smoothScrollToPosition(60);
}else if(position==3){
myRecyclerView.smoothScrollToPosition(90);
}
}
#Override
public void onTabUnselected(TabLayout.Tab tab) {
}
#Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
myRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
isUserScrolling = true;
if (isListGoingUp) {
//my recycler view is actually inverted so I have to write this condition instead
if (linearLayoutManager.findLastCompletelyVisibleItemPosition() + 1 == arrayList.size()) {
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
#Override
public void run() {
if (isListGoingUp) {
if (linearLayoutManager.findLastCompletelyVisibleItemPosition() + 1 == arrayList.size()) {
Toast.makeText(MyActivity.this, "exeute something", Toast.LENGTH_SHORT).show();
}
}
}
}, 50);
//waiting for 50ms because when scrolling down from top, the variable isListGoingUp is still true until the onScrolled method is executed
}
}
}
}
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int itemPosition = linearLayoutManager.findFirstVisibleItemPosition();
if(isUserScrolling){
if (itemPosition == 0) { // item position of uses
TabLayout.Tab tab = myTabLayout.getTabAt(0);
tab.select();
} else if (itemPosition == 30) {// item position of side effects
TabLayout.Tab tab = myTabLayout.getTabAt(1);
tab.select();
} else if (itemPosition == 60) {// item position of how it works
TabLayout.Tab tab = myTabLayout.getTabAt(2);
tab.select();
} else if (itemPosition == 90) {// item position of precaution
TabLayout.Tab tab = myTabLayout.getTabAt(3);
tab.select();
}
}
}
});
}
}
private fun syncTabWithRecyclerView() {
// Move recylerview to the position selected by user
menutablayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
if (!isUserScrolling) {
val position = tab.position
linearLayoutManager.scrollToPositionWithOffset(position, 0)
}
}
override fun onTabUnselected(tab: TabLayout.Tab) {
}
override fun onTabReselected(tab: TabLayout.Tab) {
}
})
// Detect recyclerview position and select tab respectively.
menuRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
isUserScrolling = true
} else if (newState == RecyclerView.SCROLL_STATE_IDLE)
isUserScrolling = false
}
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (isUserScrolling) {
var itemPosition = 0
if (dy > 0) {
// scrolling up
itemPosition = linearLayoutManager.findLastVisibleItemPosition()
} else {
// scrolling down
itemPosition = linearLayoutManager.findFirstVisibleItemPosition()
}
val tab = menutablayout.getTabAt(itemPosition)
tab?.select()
}
}
})
}
I figured you don't even need those flags and it was enough to override RecyclerView's onScrolled and select tab and scroll to position when tab is selected and that Tab wasn't already selected:
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val llm = recyclerView.layoutManager as LinearLayoutManager
// depending on sections'heights one may want to add more logic
// on how to determine which section to scroll to
val firstCompletePos = llm.findFirstCompletelyVisibleItemPosition()
if (firstCompletePos != tabLayout.selectedTabPosition)
tabLayout.getTabAt(firstCompletePos)?.select()
}
Then I have a TextView which is set as customView to tabLayout:
tabLayout.addTab(newTab().also { tab ->
tab.customView = AppCompatTextView(context).apply {
// set layout params match_parent, so the entire section is clickable
// set style, gravity, text etc.
setOnClickListener {
tabLayout.selectTab(tab)
recyclerView.apply {
val scrollTo = tabLayout.selectedTabPosition
smoothScrollToPosition(scrollTo)
}
}
}
})
With this setup you have:
Tab selected when user both scrolls and flings
RecyclerView is scrolled when user clicks on the tab.
I use information from another answers but here is some omissions in code, that makes it not complete and bad working. My solution 100% work without lags. Photo of complete screen you may see in the end.
This is the code inside Fragment:
private val layoutManager get() = recyclerView?.layoutManager as? LinearLayoutManager
/**
* [SmoothScroller] need for smooth scrolling inside [tabListener] of [recyclerView]
* to top border of [RecyclerView.ViewHolder].
*/
private val smoothScroller: SmoothScroller by lazy {
object : LinearSmoothScroller(context) {
override fun getVerticalSnapPreference(): Int = SNAP_TO_START
}
}
/**
* Variable for prevent calling of [RecyclerView.OnScrollListener.onScrolled]
* inside [scrollListener], when user click on [TabLayout.Tab] and
* [tabListener] was called.
*
* Fake calls happens because of [tabListener] have smooth scrolling to position,
* and when [scrollListener] catch scrolling and call [TabLayout.Tab.select].
*/
private var isTabClicked = false
/**
* Variable for prevent calling of [TabLayout.OnTabSelectedListener.onTabSelected]
* inside [tabListener], when user scroll list and function
* [RecyclerView.OnScrollListener.onScrolled] was called inside [scrollListener].
*
* Fake calls happens because [scrollListener] contains call of [TabLayout.Tab.select],
* which in turn calling click handling inside [tabListener].
*/
private var isScrollSelect = false
private val scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
/**
* Reset [isTabClicked] key when user start scroll list.
*/
if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
isTabClicked = false
}
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
/**
* Prevent scroll handling after tab click (see inside [tabListener]).
*/
if (isTabClicked) return
val commonIndex = commonIndex ?: return
val karaokeIndex = karaokeIndex ?: return
val socialIndex = socialIndex ?: return
val reviewIndex = reviewIndex ?: return
val addIndex = addIndex ?: return
when (layoutManager?.findFirstVisibleItemPosition() ?: return) {
in commonIndex until karaokeIndex -> selectTab(TabIndex.COMMON)
in karaokeIndex until socialIndex -> selectTab(TabIndex.KARAOKE)
in socialIndex until reviewIndex -> {
/**
* In case if [reviewIndex] can't reach top of the list,
* to become first visible item. Need check [addIndex]
* (last element of list) completely visible or not.
*/
if (layoutManager?.findLastCompletelyVisibleItemPosition() != addIndex) {
selectTab(TabIndex.CONTACTS)
} else {
selectTab(TabIndex.REVIEWS)
}
}
in reviewIndex until addIndex -> selectTab(TabIndex.REVIEWS)
}
}
/**
* It's very important to skip cases when [TabLayout.Tab] is checked like current,
* otherwise [tabLayout] will terribly lagging on [recyclerView] scroll.
*/
private fun selectTab(#TabIndex index: Int) {
val tab = tabLayout?.getTabAt(index) ?: return
if (!tab.isSelected) {
recyclerView?.post {
isScrollSelect = true
tab.select()
}
}
}
}
private val tabListener = object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) = scrollToPosition(tab)
override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
/*
* If user click on tab again.
*/
override fun onTabReselected(tab: TabLayout.Tab?) = scrollToPosition(tab)
private fun scrollToPosition(tab: TabLayout.Tab?) {
/**
* Prevent scroll to position calling from [scrollListener].
*/
if (isScrollSelect) {
isScrollSelect = false
return
}
val position = when (tab?.position) {
TabIndex.COMMON -> commonIndex
TabIndex.KARAOKE -> karaokeIndex
TabIndex.CONTACTS -> socialIndex
TabIndex.REVIEWS -> reviewIndex
else -> null
}
if (position != null) {
isTabClicked = true
smoothScroller.targetPosition = position
layoutManager?.startSmoothScroll(smoothScroller)
}
}
}
private val commonIndex get() = adapter.list.validIndexOfFirst { it is ClubScreenItem.Info }
private val karaokeIndex get() = adapter.list.validIndexOfFirst { it is ClubScreenItem.Karaoke }
private val socialIndex get() = adapter.list.validIndexOfFirst { it is ClubScreenItem.Social }
private val reviewIndex get() = adapter.list.validIndexOfFirst { it is ClubScreenItem.ReviewHeader }
private val addIndex get() = adapter.list.validIndexOfFirst { it is ClubScreenItem.AddReview }
Extension:
private const val ND_INDEX = -1
fun <T> List<T>.validIndexOfFirst(predicate: (T) -> Boolean): Int? {
return indexOfFirst(predicate).takeIf { it != ND_INDEX }
}
TabIndex class for getting tab by position:
#IntDef(TabIndex.COMMON, TabIndex.KARAOKE, TabIndex.CONTACTS, TabIndex.REVIEWS)
private annotation class TabIndex {
companion object {
const val COMMON = 0
const val KARAOKE = 1
const val CONTACTS = 2
const val REVIEWS = 3
}
}
And it's how looks my ClubScreenItem:
sealed class ClubScreenItem {
class Info(val data: ClubItem): ClubScreenItem()
...
class Karaoke(...): ClubScreenItem()
class Social(...): ClubScreenItem()
...
class ReviewHeader(...): ClubScreenItem()
...
object AddReview : ClubScreenItem()
}
This is how looks screen:
Try this,
Simple Step :
Detect RecyclerView scroll state
Use findFirstVisibleItemPosition() to return the adapter position of the first visible view
Change the tab based on RecyclerView item position
Done
private fun syncTabWithRecyclerView() {
var isUserScrolling = false
val layoutManager = binding.recyclerViewGroup.layoutManager as LinearLayoutManager
val tabListener = object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
val tabPosition = tab?.position
if (tabPosition != null) {
viewModel.setTabPosition(tabPosition)
// prevent RecyclerView to snap to its item start position while user scrolling,
// idk how to explain this XD
if (!isUserScrolling){
layoutManager.scrollToPositionWithOffset(tabPosition, 0)
}
}
}
override fun onTabUnselected(tab: TabLayout.Tab?) {}
override fun onTabReselected(tab: TabLayout.Tab?) {}
}
binding.tabLayout.addOnTabSelectedListener(tabListener)
// Detect recyclerview scroll state
val onScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
isUserScrolling = true
} else if (newState == RecyclerView.SCROLL_STATE_IDLE) {
isUserScrolling = false
}
}
// this just represent my tab name using enum class ,
// and ordinal is just the index of its position in enum
val hardcase3D = CaseType.HARDCASE_3D.ordinal
val softcaseBlackmatte = CaseType.SOFTCASE_BLACKMATTE.ordinal
val softcaseTransparent = CaseType.SOFTCASE_TRANSPARENT.ordinal
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (isUserScrolling) {
when (layoutManager.findFirstVisibleItemPosition()) {
in hardcase3D until softcaseBlackmatte -> {
viewModel.setTabPosition(hardcase3D)
}
in softcaseBlackmatte until softcaseTransparent -> {
viewModel.setTabPosition(softcaseBlackmatte)
}
softcaseTransparent -> {
viewModel.setTabPosition(softcaseTransparent)
}
}
}
}
}
binding.recyclerViewGroup.addOnScrollListener(onScrollListener)
}
viewModel , you can simply use liveData if you want
private var _tabPosition = MutableStateFlow(CaseType.HARDCASE_3D)
val tabPostition : StateFlow<CaseType>
get() = _tabPosition
fun setTabPosition(position: Int){
_tabPosition.value = CaseType.values()[position]
}
Observer
lifecycleScope.launch(Dispatchers.Default) {
viewModel.tabPostition.collect { caseType ->
val positionIndex = CaseType.values().indexOf(caseType)
handleSelectedTab(positionIndex)
}
}
and handleSelectedTab
private fun handleSelectedTab(index: Int) {
val tab = binding.tabLayout.getTabAt(index)
tab?.select()
}
enum
enum class CaseType(val caseTypeName:String) {
HARDCASE_3D("Hardcase 3D"),
SOFTCASE_BLACKMATTE("Softcase Blackmatte"),
SOFTCASE_TRANSPARENT("Softcase Transparent")
}
Hello I need to make the infinite scrolling (Pagination) with the recyclerview using the staggeredGridLayoutManager. Pagination is working but the problem is that onLoadMore() function is called so many times while scrolling which causing problems, here is my code:
newSearchAdapter = new NewSearchAdapter(getActivity(), gridData);
StaggeredGridLayoutManager mLayoutManager;
mLayoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
mLayoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_NONE);
rv_NewProfilesGrid.setLayoutManager(mLayoutManager);
rv_NewProfilesGrid.setAdapter(newSearchAdapter);
rv_NewProfilesGrid.addOnScrollListener(new EndlessRecyclerOnScrollListenerStaggeredLayoutmanager(mLayoutManager) {
#Override
public void onLoadMore(int current_page) {
//calling the api
}
});
and here is my scroll listner
I think the problem here is with the getFirstVisibleItems() function because with GridLayoutManger or LinearLayoutManager it returns an integer but with StaggeredLayout it returns an int Array, so I did the following:
public abstract class EndlessRecyclerOnScrollListenerStaggeredLayoutmanager extends RecyclerView.OnScrollListener {
public static String TAG = EndlessRecyclerOnScrollListenerStaggeredLayoutmanager.class.getSimpleName();
private int scrolledDistance = 0;
private boolean controlsVisible = false;
private boolean loading = true; // True if we are still waiting for the last set of data to load.
private int visibleThreshold = 5; // The minimum amount of items to have below your current scroll position before loading more.
public static boolean loadOneTime = false;
private int pastVisibleItems, visibleItemCount, totalItemCount,previousTotal;
private int current_page = 1;
private StaggeredGridLayoutManager mStaggeredGridLayoutManager;
public EndlessRecyclerOnScrollListenerStaggeredLayoutmanager(StaggeredGridLayoutManager staggeredGridLayoutManager) {
this.mStaggeredGridLayoutManager = staggeredGridLayoutManager;
}
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
visibleItemCount = recyclerView.getChildCount();
totalItemCount = mStaggeredGridLayoutManager.getItemCount();
int[] firstVisibleItems = null;
firstVisibleItems = mStaggeredGridLayoutManager.findFirstVisibleItemPositions(firstVisibleItems);
if (firstVisibleItems != null && firstVisibleItems.length > 0) {
pastVisibleItems = firstVisibleItems[0];
}
if (loading) {
if ((visibleItemCount + pastVisibleItems) >= totalItemCount) {
loading = false;
previousTotal = totalItemCount;
}
}
if (!loading && (totalItemCount - visibleItemCount)
<= (pastVisibleItems + visibleThreshold)) {
// End has been reached
// Do something
current_page++;
loadOneTime=false;
onLoadMore(current_page);
loading = true;
}
if (scrolledDistance > 1 && controlsVisible) {
controlsVisible = false;
scrolledDistance = 0;
} else if (scrolledDistance < -1 && !controlsVisible) {
controlsVisible = true;
scrolledDistance = 0;
}
if ((controlsVisible && dy > 0) || (!controlsVisible && dy < 0)) {
scrolledDistance += dy;
}
}
public abstract void onLoadMore(int current_page);
}
Try this. I borrowed it from a stack overflow answer and borrowed another answer and combine to of them I finally solve it.
import android.util.Log
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
abstract class PaginationScrollListener constructor() :
RecyclerView.OnScrollListener() {
private lateinit var mLayoutManager: RecyclerView.LayoutManager
constructor(layoutManager: GridLayoutManager) : this() {
this.mLayoutManager = layoutManager
}
constructor(layoutManager: StaggeredGridLayoutManager) : this() {
this.mLayoutManager = layoutManager
}
constructor(layoutManager: LinearLayoutManager) : this() {
this.mLayoutManager = layoutManager
}
/*
Method gets callback when user scroll the search list
*/
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val visibleItemCount = mLayoutManager.childCount
val totalItemCount = mLayoutManager.itemCount
var firstVisibleItemPosition = 0
when (mLayoutManager) {
is StaggeredGridLayoutManager -> {
val firstVisibleItemPositions =
(mLayoutManager as StaggeredGridLayoutManager).findFirstVisibleItemPositions(null)
// get maximum element within the list
firstVisibleItemPosition = firstVisibleItemPositions[0]
}
is GridLayoutManager -> {
firstVisibleItemPosition =
(mLayoutManager as GridLayoutManager).findFirstVisibleItemPosition()
}
is LinearLayoutManager -> {
firstVisibleItemPosition =
(mLayoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
}
}
if (!isLoading && !isLastPage) {
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount
&& firstVisibleItemPosition >= 0
) {
Log.i(TAG, "Loading more items")
loadMoreItems()
}
}
}
protected abstract fun loadMoreItems()
abstract val isLastPage: Boolean
abstract val isLoading: Boolean
companion object {
private val TAG = PaginationScrollListener::class.java.simpleName
}
}
And use this in your recycler view in this way
rvCategoryProducts.addOnScrollListener(object :
PaginationScrollListener(layoutManager) {
override fun loadMoreItems() {
vm.isLoading = true
vm.currentPage++
GlobalScope.launch {
vm.getCategoryProductByCatId(vm.id, vm.currentPage)
}
}
override val isLastPage: Boolean
get() = vm.isLastPage
override val isLoading: Boolean
get() = vm.isLoading
})
I have a method called switchCamera, I'm trying to switch camera from front to back on the click of a button, in one smooth transition. My application freezes when I call this method - I know I'm not doing something right. Can anyone help me out here?
Any help is much appreciated.
public void switchCamera(){
int camNum = 0;
camNum = Camera.getNumberOfCameras();
int camBackId = Camera.CameraInfo.CAMERA_FACING_BACK;
int camFrontId = Camera.CameraInfo.CAMERA_FACING_FRONT;
Camera.CameraInfo currentCamInfo = new Camera.CameraInfo();
//if camera is running
if (camera != null){
//and there is more than one camera
if (camNum > 1){
//stop current camera
camera.stopPreview();
camera.setPreviewCallback(null);
//camera.takePicture(null, null, PictureCallback);
camera.release();
camera = null;
//stop surfaceHolder?
if (currentCamInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT){
//switch camera to back camera
camera=Camera.open(camBackId);
}
else{
//switch camera to front camera
camera=Camera.open(camFrontId);
}
//switch camera back on
//specify surface?
try {
camera.setPreviewDisplay(surfaceHolder);
camera.setPreviewCallback((PreviewCallback) this);
camera.startPreview();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
Button otherCamera = (Button) findViewById(R.id.OtherCamera);
otherCamera.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (inPreview) {
camera.stopPreview();
}
//NB: if you don't release the current camera before switching, you app will crash
camera.release();
//swap the id of the camera to be used
if(currentCameraId == Camera.CameraInfo.CAMERA_FACING_BACK){
currentCameraId = Camera.CameraInfo.CAMERA_FACING_FRONT;
}
else {
currentCameraId = Camera.CameraInfo.CAMERA_FACING_BACK;
}
camera = Camera.open(currentCameraId);
setCameraDisplayOrientation(CameraActivity.this, currentCameraId, camera);
try {
camera.setPreviewDisplay(previewHolder);
} catch (IOException e) {
e.printStackTrace();
}
camera.startPreview();
}
If you want to make the camera image show in the same orientation as
the display, you can use the following code.
public static void setCameraDisplayOrientation(Activity activity,
int cameraId, android.hardware.Camera camera) {
android.hardware.Camera.CameraInfo info =
new android.hardware.Camera.CameraInfo();
android.hardware.Camera.getCameraInfo(cameraId, info);
int rotation = activity.getWindowManager().getDefaultDisplay()
.getRotation();
int degrees = 0;
switch (rotation) {
case Surface.ROTATION_0: degrees = 0; break;
case Surface.ROTATION_90: degrees = 90; break;
case Surface.ROTATION_180: degrees = 180; break;
case Surface.ROTATION_270: degrees = 270; break;
}
int result;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (info.orientation + degrees) % 360;
result = (360 - result) % 360; // compensate the mirror
} else { // back-facing
result = (info.orientation - degrees + 360) % 360;
}
camera.setDisplayOrientation(result);
}
First you need to destroy the SurfacePreview of previous camera, then need to create a new object of camera(Back/Front)
`//Code to destroy SurfacePreview
mPreview.surfaceDestroyed(mPreview.getHolder());
mPreview.getHolder().removeCallback(mPreview);
mPreview.destroyDrawingCache();
preview.removeView(mPreview);
mCamera.stopPreview();
mCamera.stopPreview();
mCamera.setPreviewCallback(null);
mCamera.release();
//Now create new camera object
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
mCamera = Camera.open(camIdx);
mPreview = new CameraPreview(CameraActivity.this, mCamera);
preview.addView(mPreview);
mCamera.setPreviewDisplay(mPreview.getHolder());
mCamera.startPreview();
}`
After a long search finally i can switched camera successfully. mjosh's answer is a useful answer but it did not worked for me. The trick i found finally is create new CameraPreview class and add it again.
Here is my CameraPreview class.
#SuppressLint("ViewConstructor")
class CameraPreview(context: Context?,
private var camera: Camera,
private val displayRotation: Int) : SurfaceView(context), SurfaceHolder.Callback {
companion object {
private const val TAG = "TAG"
private const val FOCUS_AREA_SIZE = 300
}
val surfaceHolder: SurfaceHolder = holder
private var previewSize: Camera.Size? = null
private val supportedPreviewSizes: MutableList<Camera.Size>?
init {
surfaceHolder.addCallback(this)
supportedPreviewSizes = camera.parameters.supportedPreviewSizes
}
private val surfaceViewTouchListener: View.OnTouchListener = OnTouchListener { v, event ->
camera.cancelAutoFocus()
val focusRect = calculateFocusArea(event.x, event.y)
val parameters = camera.parameters
if (parameters.focusMode == Camera.Parameters.FOCUS_MODE_AUTO) {
parameters.focusMode = Camera.Parameters.FOCUS_MODE_AUTO
}
if (parameters.maxNumFocusAreas > 0) {
val areaList = ArrayList<Camera.Area>()
areaList.add(Camera.Area(focusRect, 1000))
parameters.focusAreas = areaList
}
try {
camera.cancelAutoFocus()
camera.parameters = parameters
camera.startPreview()
camera.autoFocus { _, cam ->
if (cam.parameters.focusMode == Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE) {
val parameters = cam.parameters;
parameters.focusMode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE
if (parameters.maxNumFocusAreas > 0) {
parameters.focusAreas = null
}
camera.parameters = parameters
camera.startPreview()
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return#OnTouchListener true
}
override fun surfaceCreated(holder: SurfaceHolder?) {
setOnTouchListener(surfaceViewTouchListener)
// The Surface has been created, now tell the camera where to draw the preview.
try {
camera.setPreviewDisplay(holder)
camera.setDisplayOrientation(displayRotation)
camera.startPreview()
} catch (e: IOException) {
Log.d(TAG, "Error setting camera preview: " + e.message)
}
}
override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
// If your preview can change or rotate, take care of those events here.
// Make sure to stop the preview before resizing or reformatting it.
if (holder?.surface == null) {
// preview surface does not exist
return
}
// stop preview before making changes
try {
camera.stopPreview()
} catch (e: Exception) {
// ignore: tried to stop a non-existent preview
}
// set preview size and make any resize, rotate or
// reformatting changes here
// start preview with new settings
try {
val parameters = camera.parameters
val bestPictureSize = getBestPictureSize(width, height, parameters)
bestPictureSize?.let {
parameters.setPictureSize(it.width, it.height)
}
previewSize?.let {
parameters.setPreviewSize(it.width, it.height)
}
camera.parameters = parameters
camera.setPreviewDisplay(holder)
camera.startPreview()
} catch (e: Exception) {
Log.d(TAG, "Error starting camera preview: " + e.message)
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val width = View.resolveSize(suggestedMinimumWidth, widthMeasureSpec)
val height = View.resolveSize(suggestedMinimumHeight, heightMeasureSpec)
setMeasuredDimension(width, height)
if (supportedPreviewSizes != null) {
previewSize = getOptimalPreviewSize(supportedPreviewSizes, width, height)
}
}
private fun getOptimalPreviewSize(sizes: List<Camera.Size>?, w: Int, h: Int): Camera.Size? {
val ASPECT_TOLERANCE = 0.1
val targetRatio = h.toDouble() / w
if (sizes == null) return null
var optimalSize: Camera.Size? = null
var minDiff = java.lang.Double.MAX_VALUE
for (size in sizes) {
val ratio = size.width.toDouble() / size.height
if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue
if (Math.abs(size.height - h) < minDiff) {
optimalSize = size
minDiff = Math.abs(size.height - h).toDouble()
}
}
if (optimalSize == null) {
minDiff = java.lang.Double.MAX_VALUE
for (size in sizes) {
if (Math.abs(size.height - h) < minDiff) {
optimalSize = size
minDiff = Math.abs(size.height - h).toDouble()
}
}
}
return optimalSize
}
override fun surfaceDestroyed(holder: SurfaceHolder?) {
// no-op
}
private fun getBestPictureSize(width: Int, height: Int, parameters: Camera.Parameters): Camera.Size? {
var bestSize: Camera.Size?
val sizeList = parameters.supportedPictureSizes
bestSize = sizeList[0]
for (i in 1 until sizeList.size) {
if (sizeList[i].width * sizeList[i].height > bestSize!!.width * bestSize.height) {
bestSize = sizeList[i]
}
}
return bestSize
}
private fun calculateFocusArea(x: Float, y: Float): Rect {
val left = clamp(java.lang.Float.valueOf(x / width * 2000 - 1000).toInt(), FOCUS_AREA_SIZE)
val top = clamp(java.lang.Float.valueOf(y / height * 2000 - 1000).toInt(), FOCUS_AREA_SIZE)
return Rect(left, top, left + FOCUS_AREA_SIZE, top + FOCUS_AREA_SIZE)
}
private fun clamp(touchCoordinateInCameraReper: Int, focusAreaSize: Int): Int {
return if (Math.abs(touchCoordinateInCameraReper) + focusAreaSize / 2 > 1000) {
if (touchCoordinateInCameraReper > 0) {
1000 - focusAreaSize / 2
} else {
-1000 + focusAreaSize / 2
}
} else {
touchCoordinateInCameraReper - focusAreaSize / 2
}
}
fun turnFlashOnOrOff() {
try {
camera.stopPreview()
} catch (e: Exception) {
// ignore
}
val params = camera.parameters
params?.let {
if (params.flashMode == Camera.Parameters.FLASH_MODE_TORCH) {
params.flashMode = Camera.Parameters.FLASH_MODE_OFF
//flash.setImageResource(R.mipmap.baseline_flash_off_white_24dp)
} else {
params.flashMode = Camera.Parameters.FLASH_MODE_TORCH
//flash.setImageResource(R.mipmap.baseline_flash_on_white_24dp)
}
camera.setPreviewDisplay(holder)
try {
camera.parameters = params
} catch (e: Exception) {
e.printStackTrace()
}
camera.startPreview()
}
}
}
My openCamera method which i open camera with it:
private fun openCamera() {
camera = CameraUtil.getCameraInstance(getCameraId())
rotation = getDisplayRotation()
cameraPreview = CameraPreview(activity, camera!!, rotation)
fl_camera.addView(cameraPreview)
}
Before you create CameraPreview you have to calculate the rotation of camera and set it as displayOrientation
private fun getDisplayRotation(): Int {
val info = Camera.CameraInfo()
Camera.getCameraInfo(getCameraId(), info)
val rotation = activity.windowManager.defaultDisplay.rotation
var degrees = 0
when (rotation) {
Surface.ROTATION_0 -> degrees = 0
Surface.ROTATION_90 -> degrees = 90
Surface.ROTATION_180 -> degrees = 180
Surface.ROTATION_270 -> degrees = 270
}
var result: Int
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (info.orientation + degrees) % 360
result = (360 - result) % 360 // compensate the mirror
} else { // back-facing
result = (info.orientation - degrees + 360) % 360;
}
return result
}
And i get cameraId like below:
private fun getCameraId(): Int {
val numberOfCameras = Camera.getNumberOfCameras()
var cameraInfo: Camera.CameraInfo
for (i in 0 until numberOfCameras) {
cameraInfo = Camera.CameraInfo()
Camera.getCameraInfo(i, cameraInfo)
if (cameraInfo.facing == currentCamera) {
return i
}
}
return 0
}
And finally my SwtichCamera button works like this:
switch_camera.setOnClickListener {
try {
camera?.stopPreview()
} catch (e: Exception) {
e.printStackTrace()
}
camera?.release()
currentCamera = if (currentCamera === android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK) {
Camera.CameraInfo.CAMERA_FACING_FRONT
} else {
Camera.CameraInfo.CAMERA_FACING_BACK
}
fl_camera.removeView(cameraPreview)
openCamera()
}
This is a working solution for me. I hope this'll help you some others too.
Edit: Camera preview can be a problem for Samsung devices. Here's an alternative method for getting best preview size.
private fun getOptimalPreviewSize(sizes: List<Camera.Size>?, w: Int, h: Int): Camera.Size? {
if (sizes == null) return null
var optimalSize: Camera.Size? = null
val ratio = h.toDouble() / w
var minDiff = java.lang.Double.MAX_VALUE
var newDiff: Double
for (size in sizes) {
newDiff = Math.abs(size.width.toDouble() / size.height - ratio)
if (newDiff < minDiff) {
optimalSize = size
minDiff = newDiff
}
}
return optimalSize
}