Android how to draw shape centered in view - android

I have multiple instances of a custom view where I'm trying to draw custom shapes using the center values from the view. The problem is that I've been unsuccessful when trying to get the center and drawing shapes on it, here is the result from the code below, you can see that the black squares are not properly centered in each one of the 4 squares:
class MyCustomView
constructor(context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
private var center = PointF(0f, 0f)
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
center.set(w / 2f, h / 2f)
}
override fun onDraw(canvas: Canvas) {
val squareDimension = 100F
val halfDimension = squareDimension / 2
if (center.x == 0F || center.y == 0F) {
center = PointF(width / 2F, height / 2F)
}
val paint = Paint()
paint.color = Color.rgb(
0,
0,
0)
canvas.drawRect(
center.x - halfDimension,
center.y - halfDimension,
center.x + halfDimension,
center.y + halfDimension,
paint)
}
}
Android my layout:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:id="#+id/first"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="#id/second"
app:layout_constraintBottom_toTopOf="#id/third"
android:background="#color/colorPrimary">
<MyCustomView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:id="#+id/second"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="#id/first"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="#id/second"
android:background="#color/colorAccent">
<MyCustomView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:id="#+id/third"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="#id/fourth"
app:layout_constraintTop_toBottomOf="#id/first"
android:background="#color/colorAccent">
<MyCustomView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:id="#+id/fourth"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#id/third"
app:layout_constraintTop_toBottomOf="#id/second"
android:background="#color/colorPrimary"
>
<MyCustomView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
How can I draw the shape properly in the center of each view?

If you look at your views, MyCustomView and its parent are set to wrap_content. It is also the LinearLayouts whose bounds you see in the screen. If you inspect your views, it's likely that MyCustomViews are not actually where you expect.
Your options:
Make LinearLayout widths and heights 0dp (though from the constraints this isn't required) and make MyCustomView widths and heights match_parent.
Remove LinearLayouts altogether and replace them with MyCustomView. The parent is currently redundant, though you may have a use for it later on.
In terms of your class, things look good, though I would have a single pointf only (val), create the paint globally and not on each draw, and avoid mutating the point during onDraw. You can optionally compute a RectF during onSizeChanged and avoid any further computation during onDraw

Related

Checking if finger is over certain view not working in Android

I am working on a paint app with the following layout:
For the paint app, I detect touch events on the Canvas using onTouchEvent. I have one problem, I want to also detect touch events in which the user begins the swipe on the root and then hovers over the Canvas.
To achieve this, I added the following code:
binding.root.setOnTouchListener { _, motionEvent ->
val hitRect = Rect()
binding.activityCanvasCardView.getHitRect(hitRect)
if (hitRect.contains(motionEvent.rawX.toInt(), motionEvent.rawY.toInt())) {
binding.activityCanvasPixelGridView.onTouchEvent(motionEvent)
}
true
}
It kind of works, but the thing is. It's not detecting the touch events over the canvas (wrapped in a CardView) properly, it's like there's a sort of delay:
XML code:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/fragment_background_color_daynight"
tools:context=".activities.canvas.CanvasActivity">
<!-- This view is here to ensure that when the user zooms in, there is no overlap -->
<View
android:elevation="20dp"
android:outlineProvider="none"
android:id="#+id/activityCanvas_topView"
android:layout_width="0dp"
android:layout_height="90dp"
android:background="#color/fragment_background_color_daynight"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- The ColorSwitcherView is a view I created which helps
simplify the code for controlling the user's primary/secondary color -->
<com.therealbluepandabear.pixapencil.customviews.colorswitcherview.ColorSwitcherView
android:id="#+id/activityCanvas_colorSwitcherView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:elevation="20dp"
android:outlineProvider="none"
app:isPrimarySelected="true"
app:layout_constraintEnd_toEndOf="#+id/activityCanvas_topView"
app:layout_constraintTop_toTopOf="#+id/activityCanvas_colorPickerRecyclerView" />
<!-- The user's color palette data will be displayed in this RecyclerView -->
<androidx.recyclerview.widget.RecyclerView
android:elevation="20dp"
android:outlineProvider="none"
android:id="#+id/activityCanvas_colorPickerRecyclerView"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="#+id/activityCanvas_topView"
app:layout_constraintEnd_toStartOf="#+id/activityCanvas_colorSwitcherView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="#+id/activityCanvas_primaryFragmentHost"
tools:listitem="#layout/color_picker_layout" />
<!-- This FrameLayout is crucial when it comes to the calculation of the TransparentBackgroundView and PixelGridView -->
<FrameLayout
android:id="#+id/activityCanvas_distanceContainer"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="#+id/activityCanvas_tabLayout"
app:layout_constraintEnd_toEndOf="#+id/activityCanvas_primaryFragmentHost"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/activityCanvas_topView" />
<!-- This gives both views (the PixelGridView and TransparentBackgroundView) a nice drop shadow -->
<com.google.android.material.card.MaterialCardView
android:id="#+id/activityCanvas_cardView"
style="#style/activityCanvas_canvasFragmentHostCardViewParent_style"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="#+id/activityCanvas_tabLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/activityCanvas_topView">
<!-- At runtime, the width and height of the TransparentBackgroundView and PixelGridView will be calculated -->
<com.therealbluepandabear.pixapencil.customviews.transparentbackgroundview.TransparentBackgroundView
android:id="#+id/activityCanvas_transparentBackgroundView"
android:layout_width="0dp"
android:layout_height="0dp" />
<com.therealbluepandabear.pixapencil.customviews.pixelgridview.PixelGridView
android:id="#+id/activityCanvas_pixelGridView"
android:layout_width="0dp"
android:layout_height="0dp" />
</com.google.android.material.card.MaterialCardView>
<!-- The primary tab layout -->
<com.google.android.material.tabs.TabLayout
android:elevation="20dp"
android:outlineProvider="none"
android:id="#+id/activityCanvas_tabLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:tabStripEnabled="false"
app:layout_constraintBottom_toTopOf="#+id/activityCanvas_viewPager2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/activityCanvas_tab_tools_str" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/activityCanvas_tab_filters_str" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/activityCanvas_tab_color_palettes_str" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/activityCanvas_tab_brushes_str" />
</com.google.android.material.tabs.TabLayout>
<!-- This view allows move functionality -->
<View
android:elevation="20dp"
android:outlineProvider="none"
android:id="#+id/activityCanvas_moveView"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#android:color/transparent"
app:layout_constraintBottom_toBottomOf="#+id/activityCanvas_distanceContainer"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/activityCanvas_topView" />
<!-- The tools, palettes, brushes, and filters fragment will be displayed inside this ViewPager -->
<androidx.viewpager2.widget.ViewPager2
android:elevation="20dp"
android:outlineProvider="none"
android:id="#+id/activityCanvas_viewPager2"
android:layout_width="0dp"
android:layout_height="110dp"
app:layout_constraintBottom_toBottomOf="#+id/activityCanvas_primaryFragmentHost"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<!-- This CoordinatorLayout is responsible for ensuring that the app's snackbars can be swiped -->
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:elevation="20dp"
android:outlineProvider="none"
android:id="#+id/activityCanvas_coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<!-- All of the full page fragments will be displayed in this fragment host -->
<FrameLayout
android:elevation="20dp"
android:outlineProvider="none"
android:id="#+id/activityCanvas_primaryFragmentHost"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
How can I detect touch events properly over a view?
binding.activityCanvasCardView.getHitRect(hitRect) is in the coordinates of the view's parent. See View#getHitRect().
motionEvent.rawX and (), motionEvent.rawY are in the device display coordinates. See MotionEvent#getRawX().
The offset is going to be the difference between the two. You will need to transform one set of coordinates to the other to make the comparisons.
Use MotionEvent#getX() and MotionEvent#getY() for view coordinates.
The other problem that you may have is that since the touch listener is on the root view, the MotionEvent passed to your custom view, PixelGridView, will be in the coordinates of the root view. The custom view would have to have a way to translate those coordinates to its own coordinates to draw on its canvas properly. Maybe you are accommodating this now, but your code for that custom view is not posted.
Update: Sample coode
This is an update to the update with the sample code. Although what I posted before demonstrates the concepts, there were a few things that I thought needed to be corrected for a more complete answer. The following is the updated code.
Let's consider a simplified layout:
<layout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_optimizationLevel="none">
<com.google.android.material.card.MaterialCardView
android:id="#+id/activityCanvas_cardView"
android:layout_width="300dp"
android:layout_height="300dp"
app:cardBackgroundColor="#android:color/holo_red_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.example.starterapp.MyView
android:id="#+id/activityCanvas_pixelGridView"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_margin="50dp"
android:background="#android:color/holo_blue_light" />
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
And a simple custom view that draws a path:
class MyView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val mPath = Path()
private val mPaint = Paint().apply {
color = context.getColor(android.R.color.black)
style = Paint.Style.STROKE
strokeWidth = 5f
}
private lateinit var mViewOffset: Point
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawPath(mPath, mPaint)
}
fun addMotion(motionEvent: MotionEvent) {
for (i in 0 until motionEvent.historySize) {
addPoint(motionEvent.getHistoricalX(i), motionEvent.getHistoricalY(i))
}
addPoint(motionEvent.x, motionEvent.y)
invalidate()
}
fun startDrawing(motionEvent: MotionEvent) {
mPath.reset()
mPath.moveTo(motionEvent.x - mViewOffset.x, motionEvent.y - mViewOffset.y)
invalidate()
}
fun setViewOffset(offset: Point) {
mViewOffset = Point(offset)
}
private fun addPoint(x: Float, y: Float) {
mPath.lineTo(x - mViewOffset.x, y - mViewOffset.y)
}
}
And, finally a fragment that does the work. Comments are in the code.
class MainFragment : Fragment() {
private lateinit var binding: FragmentMainBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DataBindingUtil.setContentView(requireActivity(), R.layout.fragment_main)
binding.root.setOnTouchListener { _, motionEvent ->
when (motionEvent.action) {
MotionEvent.ACTION_DOWN ->
binding.activityCanvasPixelGridView.startDrawing(motionEvent)
MotionEvent.ACTION_MOVE ->
binding.activityCanvasPixelGridView.addMotion(motionEvent)
}
true
}
// Wait until everything is laid out so positions and sizes are known.
binding.root.doOnNextLayout {
val gridViewOffset = Point()
var view = binding.activityCanvasPixelGridView as View
while (view != it) {
gridViewOffset.x += view.left
gridViewOffset.y += view.top
view = view.parent as View
}
binding.activityCanvasPixelGridView.setViewOffset(gridViewOffset)
}
return binding.root
}
companion object {
val TAG = this::class.simpleName
}
}
When all this is executed, we see the following:

Guidelines Inside ScrollView - use Viewport %

I would like to add content to an app that starts at about 70% down vertically and can be scrolled upwards to cover the top 70% views.
I thought of using two children ConstraintLayout's inside a parent ConstraintLayout - the two children would be on top of each other. One would contain the views that would populate the first 70% of the screen while the other would contain a NestedScrollView which has an invisible <View> that takes up 70% of the height and then the additional content that can be scrolled up.
I'm facing a problem with marking the 70% spot - using a Guideline inside the NestedScrollView isn't working because the %s are fluid (it matches to 70% of the content inside the NestedScrollView instead of 70% of the viewable screen). Using a Guideline outside the NestedScrollView doesn't work because well... constraints have to be siblings to compile.
How can I accomplish this?
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/parentLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/firstConstraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/red5F"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">
// A bunch of content that should fill up the first 70% of the screen and be covered by the overlay if user scrolls
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/overlayConstraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:fillViewport="true"
android:id="#+id/scrollView"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/overlayInnerLayout">
<androidx.constraintlayout.widget.Guideline
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/verticalGuidelineOverlay"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.7"/>
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:id="#+id/spacerView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="#id/verticalGuidelineOverlay"
app:layout_constraintLeft_toLeftOf="parent"/>
// More content here that the user could scroll upwards that would start at the 70% point and eventually cover the entire screen.
</ConstraintLayout>
</NestedScrollView>
</ConstraintLayout>
</ConstraintLayout>
Video w/example here: https://imgur.com/a/BTolYUu
Try out this method,
<?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/parentLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/firstConstraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/transparent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/overlayConstraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.core.widget.NestedScrollView
android:id="#+id/scrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:weightSum="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0.7"
android:orientation="horizontal">
<RelativeLayout
android:id="#+id/transparentView"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_weight="0.3" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#drawable/bg">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="20sp"
android:text="#string/lorem_ipsum"
tools:ignore="MissingConstraints"
android:textSize="18sp"/>
</RelativeLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
set 70% height programatically using layoutParams
val transparentView = findViewById<RelativeLayout>(R.id.transparentView)
val metrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(metrics)
val height = Math.min(metrics.widthPixels, metrics.heightPixels) //height
val params = transparentView.layoutParams
params.height = (height * 70) / 70
transparentView.layoutParams = params
you will get the required result : enter link description here
Remove guidelines and use a view like this as a spacer view. It's height constrained to be 1.15 of it's width. You can change it around a littile to get what you want
<View
android:id="#+id/spacerView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="1:1.15"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
Also just as an advice
you're not supposed to use match_parent in ConstraintLayout, use 0dp and constraint it to both sides.
Top layout can be replaced with FrameLayout, cause you don't really use any constraints
You can use a customized BottomSheetDialogFragment that has a theme of Theme_Translucent_NoTitleBar, and change the y value of the root layout of the dialog whenever the user drags it up or down.
class MyDialogFragment(height: Int) : BottomSheetDialogFragment(), View.OnTouchListener {
private val outsideWindowHeight = height
private val rootLayout by lazy {
requireView().findViewById<LinearLayout>(R.id.dialog_root)
}
private var oldY = 0
private var baseLayoutPosition = 0
private var defaultViewHeight = 0
private var isClosing = false
private var isScrollingUp = false
private var isScrollingDown = false
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return BottomSheetDialog(
requireContext(),
android.R.style.Theme_Translucent_NoTitleBar
)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view: View = inflater.inflate(
R.layout.fragment_dialog, container,
false
)
view.setBackgroundResource(R.drawable.rounded_background)
(dialog as BottomSheetDialog).apply {
setCancelable(false)
behavior.peekHeight =
(outsideWindowHeight * 0.3).toInt() // Minimum height of the BottomSheet is 30% of the root layout (to leave the 70% to the main layout)
}
return view
}
#SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
rootLayout.setOnTouchListener(this)
}
#SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
// Get finger position on screen
val y = event!!.rawY.toInt()
// Switch on motion event type
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_DOWN -> {
// save default base layout height
defaultViewHeight = rootLayout.height
oldY = y
baseLayoutPosition = rootLayout.y.toInt()
}
MotionEvent.ACTION_UP -> {
// If user was doing a scroll up
if (isScrollingUp) {
// Reset baselayout position
rootLayout.y = 0f
// We are not in scrolling up anymore
isScrollingUp = false
}
// If user was doing a scroll down
if (isScrollingDown) {
// Reset baselayout position
rootLayout.y = 0f
// Reset base layout size
rootLayout.layoutParams.height = defaultViewHeight
rootLayout.requestLayout()
// We are not in scrolling down anymore
isScrollingDown = false
}
}
MotionEvent.ACTION_MOVE -> {
if (rootLayout.y <= -100) {
return true
}
if (!isClosing) {
val currentYPosition = rootLayout.y.toInt()
// If we scroll up
if (oldY > y) {
// First time android rise an event for "up" move
if (!isScrollingUp) {
isScrollingUp = true
}
rootLayout.y = rootLayout.y + (y - oldY)
} else {
// First time android rise an event for "down" move
if (!isScrollingDown) {
isScrollingDown = true
}
// change position because view anchor is top left corner
rootLayout.y = rootLayout.y + (y - oldY)
rootLayout.requestLayout()
}
// Update position
oldY = y
}
}
}
return true
}
}
fragment_dialog.xml (Nothing fancy):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/dialog_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="#+id/tv_bottom_sheet_heading"
android:layout_width="wrap_content"
android:layout_height="#dimen/dp_56"
android:layout_marginStart="#dimen/dp_16"
android:layout_marginEnd="#dimen/dp_16"
android:gravity="center"
android:text="#string/bottom_sheet_option_heading"
android:textColor="#android:color/black"
android:textSize="16sp" />
<TextView
android:id="#+id/tv_btn_add_photo_camera"
android:layout_width="match_parent"
android:layout_height="#dimen/dp_48"
android:layout_marginStart="#dimen/dp_16"
android:layout_marginEnd="#dimen/dp_16"
android:backgroundTint="#android:color/white"
android:drawableStart="#drawable/ic_camera_alt_black_24dp"
android:drawableLeft="#drawable/ic_camera_alt_black_24dp"
android:drawablePadding="#dimen/dp_32"
android:drawableTint="#color/md_bottom_sheet_text_color"
android:gravity="start|center_vertical"
android:text="#string/bottom_sheet_option_camera"
android:textColor="#color/md_bottom_sheet_text_color"
android:textSize="16sp" />
<TextView
android:id="#+id/tv_btn_add_photo_gallery"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginStart="#dimen/dp_16"
android:layout_marginEnd="#dimen/dp_16"
android:backgroundTint="#android:color/white"
android:drawableStart="#drawable/ic_insert_photo_black_24dp"
android:drawableLeft="#drawable/ic_insert_photo_black_24dp"
android:drawablePadding="#dimen/dp_32"
android:drawableTint="#color/md_bottom_sheet_text_color"
android:gravity="start|center_vertical"
android:text="#string/bottom_sheet_option_gallery"
android:textColor="#color/md_bottom_sheet_text_color"
android:textSize="16sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="#dimen/md_bottom_sheet_separator_top_margin"
android:layout_marginBottom="#dimen/dp_8"
android:background="#color/grayTextColor" />
<TextView
android:id="#+id/tv_btn_remove_photo"
android:layout_width="match_parent"
android:layout_height="#dimen/dp_48"
android:layout_marginStart="#dimen/dp_16"
android:layout_marginEnd="#dimen/dp_16"
android:backgroundTint="#android:color/white"
android:drawableStart="#drawable/ic_delete_black_24dp"
android:drawableLeft="#drawable/ic_delete_black_24dp"
android:drawablePadding="#dimen/dp_32"
android:drawableTint="#color/md_bottom_sheet_text_color"
android:gravity="start|center_vertical"
android:text="#string/bottom_sheet_option_remove_photo"
android:textColor="#color/md_bottom_sheet_text_color"
android:textSize="16sp" />
<com.google.android.material.button.MaterialButton
android:id="#+id/btn_material"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Material button"
android:textAppearance="#style/TextAppearance.AppCompat.Medium" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#string/longText1"
android:textColor="#color/white"
android:textSize="22sp" />
</LinearLayout>
And send the height of the root ViewGroup of the main layout to the dialog in the main activity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val root = findViewById<ConstraintLayout>(R.id.root)
root.viewTreeObserver.addOnGlobalLayoutListener(object :
OnGlobalLayoutListener {
override fun onGlobalLayout() {
root.viewTreeObserver
.removeOnGlobalLayoutListener(this)
val dialogFragment = MyDialogFragment(root.height)
dialogFragment.show(supportFragmentManager, "dialog_tag")
}
})
}
}
Preview:

Why does my extended EditText not respond like an EditText?

I created a class that extends EditText and forces them into a square shape with a border. I cannot click on nor edit the text in any of these. When I change the 4 SquareCell views back to EditText views, I am able to edit them (but obviously lose the square format). Why can't I edit SquareCell views?
SquareCell class
package com.example.autogrid
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatEditText
class SquareCell #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : androidx.appcompat.widget.AppCompatEditText(context, attrs, defStyleAttr) {
private var squareSize = 0f
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLACK
style = Paint.Style.STROKE
strokeWidth = 5f
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//find smaller dimension between width and height and set layout size to it
val smallerDim = Math.min(
MeasureSpec.getSize(widthMeasureSpec),
MeasureSpec.getSize(heightMeasureSpec)
)
super.onMeasure(
MeasureSpec.makeMeasureSpec(smallerDim, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(smallerDim, MeasureSpec.EXACTLY)
)
squareSize = smallerDim.toFloat()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//add border
canvas.drawRect(0f, 0f, squareSize, squareSize, paint)
}
}
activity_main.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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.example.autogrid.SquareCell
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Edit Text 00"/>
<com.example.autogrid.SquareCell
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Edit Text 01"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.example.autogrid.SquareCell
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Edit Text 10"/>
<com.example.autogrid.SquareCell
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Edit Text 11"/>
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Result with SquareCells (not editable):
Result when I use EditTexts (editable but not squares):
Actually, you don't need to use that library you can archive these things throw a custom shape, and set that background to editText.
Here is the shape code for reference
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FFFFFF"/>
<stroke android:width="1dp"android:color="#F0F0F0" />
</shape>

How to measure bitmap with android "dp"

Basically i have an Activity that contains a CameraPreview and i put a RelativeLayout with shape drawable as its background to show rectangle on the middle of the screen.
I wanted to crop the boxed area and i managed to do it by cropping the bitmap like so
val height = dpToPx(300)
val width = dpToPx(187)
var y = srcBmp.height / 2 - (height / 2)
var x = srcBmp.width / 2 - (width / 2)
dstBmp = Bitmap.createBitmap(
srcBmp,
x,
y,
width,
height
)
It worked in most devices, but i don't know why some devices are not respecting the resolution of the DP that i defined, some pictures are bigger than the BOX in some devices.
this is how i convert dp to px
fun dpToPx(dp: Int): Int {
val density = this.resources
.displayMetrics
.density
return Math.round(dp.toFloat() * density)
}
this is my activity layout
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.camerakit.CameraKitView
android:layout_centerInParent="true"
android:id="#+id/camera_view_box1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:keepScreenOn="true"
app:camera_facing="back"
app:camera_focus="continuous"
app:camera_permissions="camera"/>
<RelativeLayout
android:layout_centerInParent="true"
android:background="#drawable/transparent_bg"
android:layout_width="300dp"
android:layout_height="187dp"/>
<RelativeLayout
android:focusable="true"
android:clickable="true"
android:id="#+id/take_picture_box"
android:layout_centerHorizontal="true"
android:layout_marginBottom="30dp"
android:layout_alignParentBottom="true"
android:layout_width="70dp"
android:layout_height="70dp"
android:background="#drawable/circle_bg"/>
</RelativeLayout>
What did i do wrong, and how can i fix it?

Drawing a cigarette with basic shapes

I'm trying to draw a cigarette with the "ash" view being a percentage of the "smokeable" view. It needs to be a percentage because I am going to use a slider which will change the percentage of the "ash" view.
Here is what I want:
Here is my code:
<LinearLayout
android:id="#+id/cigarette"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_marginBottom="20dp"
android:orientation="horizontal">
<View
android:id="#+id/butt"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight=".25"
android:background="#ff7700" />
<LinearLayout
android:id="#+id/smokeable"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight=".75"
android:background="#cccccc"
android:orientation="horizontal">
<View
android:id="#+id/ash"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight=".33"
android:background="#777777"
android:gravity="right" />
</LinearLayout>
</LinearLayout>
In this example I want the ash to be 33% of the smokeable view. I can't figure out why this isn't working. Any help would be appreciated.
I think the easiest cleanest way would be to make a custom View for you "cigarette", it would be composed of filter/smokeable/ash, each part with a different color.
So declare some styleable attributes in, say, attrs.xml
<resources>
<declare-styleable name="CigaretteView">
<attr name="filterColor" format="color" />
<attr name="smokeableColor" format="color" />
<attr name="ashColor" format="color" />
<attr name="filterWidth" format="dimension" />
</declare-styleable>
</resources>
The View class itself:
class CigaretteView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
companion object {
private val DEFAULT_FILTER_COLOR = 0xffbbaa00.toInt()
private val DEFAULT_SMOKABLE_COLOR = 0xffffffff.toInt()
private val DEFAULT_ASH_COLOR = 0xff888888.toInt()
private val DEFAUALT_FILTER_WIDTH_PX = 120
}
private val filterPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val smokeablePaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val ashPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private var filterWidthPx = DEFAUALT_FILTER_WIDTH_PX
private val rect = Rect()
var smokedPercent = 0f
set(value) {
field = value
invalidate()
}
init {
val a = context.theme.obtainStyledAttributes(attrs, R.styleable.CigaretteView, 0, 0)
var filterColor = DEFAULT_FILTER_COLOR
var smokeableColor = DEFAULT_SMOKABLE_COLOR
var ashColor = DEFAULT_ASH_COLOR
try {
filterColor = a.getColor(R.styleable.CigaretteView_filterColor, filterColor)
smokeableColor = a.getColor(R.styleable.CigaretteView_smokeableColor, smokeableColor)
ashColor = a.getColor(R.styleable.CigaretteView_ashColor, ashColor)
filterWidthPx = a.getDimensionPixelSize(R.styleable.CigaretteView_filterWidth, filterWidthPx)
} finally {
a.recycle()
}
filterPaint.color = filterColor
smokeablePaint.color = smokeableColor
ashPaint.color = ashColor
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
rect.set(0, 0, filterWidthPx, height)
canvas.drawRect(rect, filterPaint)
val leftOver = (width - filterWidthPx).toFloat()
val ashStartPoint = (width - leftOver * smokedPercent).toInt()
rect.set(filterWidthPx, 0, ashStartPoint, height)
canvas.drawRect(rect, smokeablePaint)
rect.set(ashStartPoint, 0, width, height)
canvas.drawRect(rect, ashPaint)
}
}
Your xml layout would be simplified to
<com.example.CigaretteView
android:id="#+id/cigaretteView"
android:layout_width="match_parent"
android:layout_height="48dp"
app:filterWidth="48dp"
app:filterColor="#ffbbaa00"
app:smokeableColor="#ffffffff"
app:ashColor="#ff888888"/>
And then you could simply assign ash percent value with
myCigaretteView.smokedPercent = percent
where percent goes from 0 (cigarette is intact) to 1 (cigarette completely smoked).
The inner LinearLayout needs another child View for the not-yet-consumed part of the cigarette. Its layout_weight has to be 1 - 0.33 = 0.67 in your example (the sum of the weights of both siblings always has to be 1).
<LinearLayout
android:id="#+id/cigarette"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_below="#+id/titleText"
android:orientation="horizontal">
<View
android:id="#+id/butt"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight=".25"
android:background="#ff7700" />
<LinearLayout
android:id="#+id/smokeable"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight=".75"
android:orientation="horizontal">
<View
android:id="#+id/rest"
android:layout_width="0dp"
android:layout_height="match_parent"
android:background="#cccccc"
android:layout_weight=".67"
/>
<View
android:id="#+id/ash"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight=".33"
android:background="#777777"
android:gravity="right" />
</LinearLayout>
</LinearLayout>
Please note that while this will fix your code, the "nested weighted LinearLayout" approach isn't a good way to go, even if modern devices should not run into performance problems.
The solution by lelloman involving a custom View is much better because you need only one View, not two ViewGroups plus three Views. You have one method to easily change the percentage, plus all the calculating and drawing parts are nicely encapsulated.
Given that all three elements are in line, you only want and need one LinearLayout parent:
<LinearLayout
android:id="#+id/cigarette"
android:layout_width="match_parent"
android:layout_height="20dp"
android:orientation="horizontal">
<View
android:id="#+id/butt"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="25"
android:background="#ff7700" />
<View
android:id="#+id/smokeable"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="65"
android:background="#cccccc" />
<View
android:id="#+id/ash"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="10"
android:background="#777777" />
</LinearLayout>
Here's the simplest thing I can think of that does what you need:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="20dp"
android:background="#777777"
android:orientation="horizontal">
<View
android:id="#+id/butt"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.25"
android:background="#ff7700"/>
<View
android:id="#+id/smokeable"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.75"
android:background="#cccccc"
android:transformPivotX="0px"/>
</LinearLayout>
Now you can call smokeable.setScaleX() with any percentage to get the desired result.

Categories

Resources