How to drag to pan a view in Android? - android

I have a LinearLayoutCompat that doesn't fit into the screen. So, I decided to implement 'drag to pan' feature for the user to scroll to any part of the layout (in all directions). It's suggested that we should do it like the code snippet on Android's official website:
// The current viewport. This rectangle represents the currently visible
// chart domain and range.
private val mCurrentViewport = RectF(AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX, AXIS_Y_MAX)
// The current destination rectangle (in pixel coordinates) into which the
// chart data should be drawn.
private val mContentRect: Rect? = null
private val mGestureListener = object : GestureDetector.SimpleOnGestureListener() {
...
override fun onScroll(
e1: MotionEvent,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
// Scrolling uses math based on the viewport (as opposed to math using pixels).
mContentRect?.apply {
// Pixel offset is the offset in screen pixels, while viewport offset is the
// offset within the current viewport.
val viewportOffsetX = distanceX * mCurrentViewport.width() / width()
val viewportOffsetY = -distanceY * mCurrentViewport.height() / height()
// Updates the viewport, refreshes the display.
setViewportBottomLeft(
mCurrentViewport.left + viewportOffsetX,
mCurrentViewport.bottom + viewportOffsetY
)
}
return true
}
}
The problem is I don't understand what the mCurrentViewPort variable is and how I should obtain the values for AXIS_X_MIN, AXIS_Y_MIN, AXIS_X_MAX and AXIS_Y_MAX to pass to the RecF object. Also, setViewPortBottomLeft(...) is marked as unresolved reference for me.

Related

Could somebody explain to me how this custom View code works?

class DrawingView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {
private lateinit var mDrawPath: FingerPath
private lateinit var mCanvasBitmap: Bitmap
private lateinit var mCanvasPaint: Paint
private lateinit var mDrawPaint: Paint
private var mBrushSize = 0
private var color = Color.BLACK
private lateinit var canvas: Canvas
init {
setUpDrawing()
}
private fun setUpDrawing() {
mDrawPaint = Paint()
mDrawPath = FingerPath(color, mBrushSize.toFloat())
mDrawPaint.color = color
mDrawPaint.style = Paint.Style.STROKE
mDrawPaint.strokeJoin = Paint.Join.ROUND
mDrawPaint.strokeCap = Paint.Cap.ROUND
mCanvasPaint = Paint(Paint.DITHER_FLAG)
mBrushSize = 20
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mCanvasBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
canvas = Canvas(mCanvasBitmap)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(mCanvasBitmap, 0f, 0f, mDrawPaint)
if (!mDrawPath.isEmpty) {
mDrawPaint.strokeWidth = mDrawPath.brushThickness
mDrawPaint.color = mDrawPath.color
canvas.drawPath(mDrawPath, mDrawPaint)
}
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
val touchX = event?.x
val touchY = event?.y
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
mDrawPath.color = color
mDrawPath.brushThickness = mBrushSize.toFloat()
mDrawPath.reset()
mDrawPath.moveTo(touchX!!, touchY!!)
}
MotionEvent.ACTION_MOVE -> {
mDrawPath.lineTo(touchX!!, touchY!!)
}
MotionEvent.ACTION_UP -> {
mDrawPath = FingerPath(color, mBrushSize.toFloat())
}
else -> return false
}
invalidate()
return true
}
internal inner class FingerPath(var color: Int, var brushThickness: Float) : Path()
}
So, I'm taking a course about Android Development and the instructor is building a drawing app -- and I can't understand how to code works, because he's not explaining the 'why'; so, he types a lot of code without explaining why he uses that variable or why is he overring those functions; therefore, I don't understand how to code work. Could you help me in understanding how this code work?
class DrawingView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {
private lateinit var mDrawPath: FingerPath
private lateinit var mCanvasBitmap: Bitmap
private lateinit var mCanvasPaint: Paint
private lateinit var mDrawPaint: Paint
private var mBrushSize = 0
private var color = Color.BLACK
private lateinit var canvas: Canvas
init {
// init block will called first when instance will be created so we
// are calling method setUpDrawing() as it is initialising everything
// that required to draw like color , brush size , brush behaviour
// (round , stroke etc .. ) . in simple manner , we can say painter is
// collecting all required tools before starting to paint
setUpDrawing()
}
private fun setUpDrawing() {
mDrawPaint = Paint()
mDrawPath = FingerPath(color, mBrushSize.toFloat())
mDrawPaint.color = color
mDrawPaint.style = Paint.Style.STROKE
mDrawPaint.strokeJoin = Paint.Join.ROUND
mDrawPaint.strokeCap = Paint.Cap.ROUND
mCanvasPaint = Paint(Paint.DITHER_FLAG)
mBrushSize = 20
}
// this method is going to be called by system when size is going to be
// changed so we are here creating blank board on which we are going to
// draw
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mCanvasBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
canvas = Canvas(mCanvasBitmap)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(mCanvasBitmap, 0f, 0f, mDrawPaint)
if (!mDrawPath.isEmpty) {
mDrawPaint.strokeWidth = mDrawPath.brushThickness
mDrawPaint.color = mDrawPath.color
canvas.drawPath(mDrawPath, mDrawPaint) // drawing path on canvas
}
}
// this method will be called by system when user is going to touch screen
override fun onTouchEvent(event: MotionEvent?): Boolean {
val touchX = event?.x
val touchY = event?.y
when (event?.action) {
// this event will be fired when user put finger on screen
MotionEvent.ACTION_DOWN -> {
mDrawPath.color = color
mDrawPath.brushThickness = mBrushSize.toFloat()
mDrawPath.reset() // reseting path before we set inital point
mDrawPath.moveTo(touchX!!, touchY!!)// set point from where drawing will be started
}
// this event will be fired when user start to move it's fingure . this will be fired continually until user pickup fingure
MotionEvent.ACTION_MOVE -> {
mDrawPath.lineTo(touchX!!, touchY!!)
}
// this event will be fired when user will pick up fingure from screen
MotionEvent.ACTION_UP -> {
mDrawPath = FingerPath(color, mBrushSize.toFloat())
}
else -> return false
}
invalidate() / /refreshing layout to reflect drawing changes
return true
}
internal inner class FingerPath(var color: Int, var brushThickness: Float) : Path()
}
This code is a custom view class in Android for drawing. The class extends the View class and implements touch event handling to allow a user to draw on the screen.
The class defines instance variables to store the paint, path, bitmap and canvas used for drawing. It also has variables for brush size and color.
In the setUpDrawing method, the paint object for drawing and the path object for tracking the user's touch are set up.
In onSizeChanged, the bitmap object is created and its canvas object is initialized.
In onDraw, the canvas is drawn on the screen and the path is drawn on the canvas if it's not empty.
The onTouchEvent method handles touch events (DOWN, MOVE, UP) and updates the path accordingly.
The FingerPath inner class extends the Path class and contains the color and brush size of the path.
Have you read through this section of the docs? Custom View Components
It explains the basics of creating a custom view, which on a basic level means overriding onDraw (where you draw the contents of the view) and maybe onSizeChanged (where you're told how big the view is - maybe you want to set the thickness of your paint brushes, work out where the centre of the view is etc).
onTouchEvent is a little more advanced, that allows you to handle the user poking at your view - the link up there gets into it, but you'll have a better understanding if you read the section on touch gestures. Broadly, you can get info about interaction events through onTouchEvent and work out things like whether the user touched the view, moved a finger, lifted it or moved it out of bounds etc. You'd use this info to, say, draw stuff under the user's finger for a painting app.
A Canvas is a drawing interface for a Bitmap. Instead of having to work out what pixels to change to what colour to draw a line or whatever, you do all your drawing through a Canvas that wraps that Bitmap. This lets you abstract the process and do high-level stuff like "draw a line", "draw an arc", "draw this entire path", "render this text" and so on. The Canvas works out how to make that happen in the Bitmap's pixel array.
The drawing happens with Paints, which you can think of as an object that says how the drawing happens. What does drawing a line look like? The Paint you use defines things like stroke width, transparency, colour etc - it's like the tool options in a paint program. It also allows you to set things like text size (if you're using it to render text) and do fancy things with blending over the pixels it's being drawn on, create shaders etc. It can also do things like control tiling if you're using a bitmap as the source image.
Generally you'll want to create these Paints at startup and reuse them during your View's lifespan, so it's typical to have a few of them - the one for painting the background, the one for drawing curves, the one for rendering text etc.
Usually in onDraw you just draw the view in its current state. The method takes a Canvas parameter - that's how you draw to the actual bitmap rendered on the UI. So if you had a knob, you could draw the circle, draw the position indicator at the appropriate location, add any text, and you're done!
For a drawing app it's a little more complicated - that "state" you need to remember and draw is the sum of all the drawing operations that have happened. You can get fancy with this (to create levels of undo) but the simplest approach is to create your own Bitmap to store the current drawing. You wrap that in its own Canvas (so you can draw on it) and then all the user interaction draws stuff to that Canvas, so you're painting on the bitmap. Then, in onDraw, you basically just need to paint the contents of that bitmap onto the system-provided Canvas so it gets drawn on-screen - just showing what you have, basically.
The code you have here is a little weird - you have that internal bitmap (called mCanvasBitmap) wrapped in a Canvas (called canvas) but you never actually draw to it. I feel like this line is meant to be rendering the Path you build up with the touch gestures:
override fun onDraw(canvas: Canvas) {
...
if (!mDrawPath.isEmpty) {
...
canvas.drawPath(mDrawPath, mDrawPaint) // drawing path on canvas
}
}
but in onDraw's scope canvas is referring to that parameter being passed in, i.e. the Canvas for drawing to the screen, not the one wrapping mCanvasBitmap. So that internal bitmap always stays empty, and then you just draw the most recent path (this is a good example of watching how you name things - why not mCanvas to be consistent?). It never "builds up" an image by drawing paths onto your internal bitmap. I can't tell what the exact intent is from the code though, I just thought I'd point it out!

How to get the offset of scroll position relative to the current text view's start?

I have a RecyclerView that contains TextViews. The number of TextViews can vary and the size of them vary as well and can be dynamically changed.
When the user scrolls to a certain position within the list and exits app, I want to be able to return to that exact position in the next session.
To do this, I need to know how many pixels have scrolled past from where the current TextView in view started and where the current position of the scroll is. For example, if the user has the 3rd TextView in view and scrolls 100 pixels down from where that TextView started, I will be able to return to this spot with scrollToPositionWithOffset(2, 100). If the TextView changes size (due to font changes), I can also return to the same spot by calculating the percentage of offset using the TextView's height.
Problem is, I cannot get the offset value in any accurate manor.
I know I can keep a running calculation on the Y value scrolled using get scroll Y of RecyclerView or Webview, but this does not give me where the TextView actually started. I can listen to when the user scrolled past the start of any TextView and record the Y position there but this will be inaccurate on fast scrolling.
Is there a better way?
Don't use position in pixels, use the index of the view. Using layout manager's findFirstVisibleItemPosition or findFirstCompletelyVisibleItemPosition.
That's a very popular question, although it may be intuitive to think and search for pixels not index.
Get visible items in RecyclerView
Find if the first visible item in the recycler view is the first item of the list or not
how to get current visible item in Recycler View
A good reason to not trust pixels is that it's not useful on some situations where index is, like rotating the screen, resizeing/splitting the app size to fit other apps side by side, foldable phones, and changing text / screen resolution.
I solved this by converting to a ListView:
lateinit var adapterRead: AdapterRead // Custom Adapter
lateinit var itemListView: ListView
/*=======================================================================================================*/
// OnViewCreated
itemListView = view.findViewById(R.id.read_listview)
setListView(itemListView)
// Upon entering this Fragment, will automatically scroll to saved position:
itemListView.afterMeasured {
scrollToPosition(itemListView, getPosition(), getOffset())
}
itemListView.setOnScrollListener(object : AbsListView.OnScrollListener {
private var currentFirstVisibleItem = 0
var offset = 0
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
// When scrolling stops, will save the current position and offset:
if(scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
offset = if(itemListView.getChildAt(0) == null) 0 else itemListView.getChildAt(0).top - itemListView.paddingTop
saveReadPosition(getReadPosition(itemListView), offset)
}
}
override fun onScroll(view: AbsListView, firstVisibleItem: Int, visibleItemCount: Int, totalItemCount: Int) {
currentFirstVisibleItem = firstVisibleItem
}
})
/*=======================================================================================================*/
// Thanks to https://antonioleiva.com/kotlin-ongloballayoutlistener/ for this:
inline fun <T : View> T.afterMeasured(crossinline f: T.() -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if(measuredWidth > 0 && measuredHeight > 0) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
f()
}
}
})
}
/*=======================================================================================================*/
fun setListView(lv: ListView) {
adapterRead = AdapterRead(list, context!!)
lv.apply {this.adapter = adapterRead}
}
/*=======================================================================================================*/
fun scrollToPosition(lv: ListView, position: Int, offset: Int) {
lv.post { lv.setSelectionFromTop(position, offset) }
}
/*=======================================================================================================*/
fun saveReadPosition(position: Int, offset: Int) {
// Persist your data to database here
}
/*=======================================================================================================*/
fun getPosition() {
// Get your saved position here
}
/*=======================================================================================================*/
fun getOffse() {
// Get your saved offset here
}

Converting milliseconds to pixels

I'm building an EPG layout from scratch and I want to calculate the width of the cells for the programs based on their runtime. I thought the best way to go about this is converting them to pixels and setting the width of the item to that. But it's coming up very small (maybe 5pixels at most). Is there a way to achieve this? Even better if I can use DP instead of PX. Below is the formula I'm using to do the conversion
fun convertMillisecondsToPx(millis: Float, context: Context): Float =
millis * pxPerMinConstant(context) / TimeUnit.MINUTES.toMillis(1)
with pxPerMinConstant(context)
fun pxPerMinConstant(context: Context): Float =
convertDpToPixel(context.resources.getDimension(R.dimen.epg_width_one_min), context)
and convertDpToPixel
fun convertDpToPixel(dp: Float, context: Context): Float =
applyDimension(COMPLEX_UNIT_DIP, dp, context.resources.displayMetrics)
and dimen_epg_width_one_min = 2dp

Why does the Jama matrix inner dimension agree in the first iteration, but later it does not?

Following Jama Matrices are defined in my code:
P: 3*3 Matrix
I: 3*3 identity Matrix
K: 3*2 Matrix
H: 2*3 Matrix
Q: 3*3 Matrix
Following is my code snippet:
private Matrix getP() {
P= (I.minus(K.times(H))).times(Q);
Log.d("csv", "P is calculated");
return P;
}
While running the code, at first iteration it works, i.e, P is calculated is printed at the Logcat. However, it happens only once and the application gets stopped. Following is the error:
java.lang.IllegalArgumentException: Matrix inner dimensions must agree.
If the Matrix inner dimension was the error, how come it runs for the first iteration? I obtained some information about the inner dimension at this link. However, I could not figure out the solution. When the equation is manually checked, the matrix dimension matches.
Anything wrong with my approach??
Thank you.
Do you mind showing how you are calling getP? The following works no matter how many times I click on the fab button.
class MainActivity : AppCompatActivity() {
val I = Matrix.identity(3,3)
val K = Matrix(3,2,5.0)
val H = Matrix(2,3,7.0)
val Q = Matrix(3,3,8.0)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
fab.setOnClickListener { view ->
getP()
}
}
private fun getP():Matrix{
val P = (I.minus(K.times(H))).times(Q)
Log.d("MainActivity","P is calculated")
return P
}
}
When getP returns where are you storing the results? Are you possibly overwriting one of the matrices?
Update
If your situation is such that making the variables final is not an option for you, then you can log each matrix's dimension and then debug the one that changes.
private fun getP():Matrix{
Log.d(TAG,"I dimension: ${I.rowDimension} x ${I.columnDimension}")
Log.d(TAG,"K dimension: ${K.rowDimension} x ${K.columnDimension}")
Log.d(TAG,"H dimension: ${H.rowDimension} x ${H.columnDimension}")
Log.d(TAG,"Q dimension: ${Q.rowDimension} x ${Q.columnDimension}")
val P = (I.minus(K.times(H))).times(Q)
Log.d(TAG,"P is calculated")
return P
}

How to implement pan and zoom on the entire RecyclerView

I have my own custom view that extends RecyclerView and I use it to render a document with multiple pages. Each item in the adapter is a page.
I want to add the ability to zoom and pan the entire document (not each page individually).
I have tried following the Android guide here but I still can't get both zoom and pan to work properly.
My current approach is this:
Have a GestureDetector to detect pan gestures
Have a ScaleGestureDetector to detect pinch gestures
When each of these detectors register events, save the information regarding the current scale factor and the pan, call invalidate() and modify the Canvas in the drawChild method appropriately.
It is the last step that I'm unsure how to implement properly.
I'm using this data class to keep track of the view's current pan and zoom:
private data class PanAndZoom(
var scaleFactor: Float,
var focusX: Float,
var focusY: Float,
var panX: Float ,
var panY: Float
)
// field in my custom view initialised like this:
private val panAndZoom = PanAndZoom(1f, 0f, 0f, 0f, 0f)
and here is how I update the values when gestures are received:
private val scaleGestureDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
panAndZoom.scaleFactor *= detector.scaleFactor
panAndZoom.focusX = detector.focusX
panAndZoom.focusY = detector.focusY
invalidate()
return true
}
}
private val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
panAndZoom.panX -= distanceX
panAndZoom.panY -= distanceY
invalidate()
return true
}
}
I override two methods in my custom view, one to pass touch events to my gesture detectors and one to draw child view:
override fun onTouchEvent(e: MotionEvent): Boolean {
gestureDetector.onTouchEvent(e)
scaleGestureDetector.onTouchEvent(e)
return super.onTouchEvent(e)
}
override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean {
val save = canvas.save()
canvas.scale(panAndZoom.scaleFactor, panAndZoom.scaleFactor, panAndZoom.focusX, panAndZoom.focusY)
canvas.translate(panAndZoom.panX, panAndZoom.panY)
val result = super.drawChild(canvas, child, drawingTime)
canvas.restoreToCount(save)
return result
}
However this doesn't give the expected result, namely:
I am able to overscroll outside the RV altogether (I realise I'm not restricting my pan values, but how do I do that)?
Pan seems to be going very fast when zoomed in (how do I scale my pan values so that they respect the scale factor)?
If I am on page 2 of the document, and I zoom in, then pan to page 3, the layout manager doesn't realise that the view was actually scrolled (meaning, calling findFirstVisibleItem() will return the second page when it isn't actually visible.
Most likely related to the point above: I can pan past a page, and because the layout manager doesn't realise that another view is now displayed it doesn't render it (because it still thinks that view is hidden from view and so it optimises by not rendering it).

Categories

Resources