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:
Related
Hello I try to add view binding in my custom view but unfortunately my custom view is not visible, every thing works fine but when I open the activity the custom view is not displayed
my custom view class
class StickerView #JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet?=null,
defStyle:Int = 0
) : ConstraintLayout(context,attributeSet,defStyle),View.OnClickListener {
private val binding = ItemStickerBinding.inflate(LayoutInflater.from(context),this,true)
private var segmentedVerticalSeekBar: SegmentedVerticalSeekBar? = null
private var btn1: ImageButton?=null
private var btn2: ImageButton?=null
private var btn3: ImageButton?=null
private var btn4: ImageButton?=null
private var btn5: ImageButton?=null
private var btn6: ImageButton?=null
private var btn0: ImageButton?=null
init {
init(context)
}
override fun onClick(v: View?) {
when(v?.id){
R.id.appCompatButton ->{
}
R.id.appCompatButton1 ->{
}
R.id.appCompatButton2 ->{
}
R.id.appCompatButton3 ->{
}
R.id.appCompatButton4 ->{
}
R.id.appCompatButton5 ->{
}
R.id.appCompatButton6 ->{
}
}
}
private fun init(context: Context){
inflate(context,R.layout.item_sticker,this)
segmentedVerticalSeekBar = binding.svsLevelView
btn0 = binding.appCompatButton
btn1 = binding.appCompatButton1
btn2 = binding.appCompatButton2
btn3 = binding.appCompatButton3
btn4 = binding.appCompatButton4
btn5 = binding.appCompatButton5
btn6 = binding.appCompatButton6
btn6?.setOnClickListener(this)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
setMeasuredDimension(Int.MAX_VALUE,400)
}
}
main activity
<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:orientation="vertical"
android:background="#color/background"
tools:context=".MainActivity">
<com.example.emoticker.StickerView
android:id="#+id/stickerView"
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">
<include layout="#layout/item_sticker"/>
</com.example.emoticker.StickerView>
</androidx.constraintlayout.widget.Constraint
my customview layout
item_sticker.xml
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="150dp"
xmlns:tools="http://schemas.android.com/tools"
android:background="#color/background"
tools:context=".StickerView"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.ss.svs.SegmentedVerticalSeekBar
android:id="#+id/svsLevelView"
android:layout_width="60dp"
android:layout_height="0dp"
android:layout_gravity="center"
android:layout_marginStart="20dp"
android:layout_marginTop="15dp"
android:layout_marginBottom="15dp"
app:backgroundColor="#color/white"
app:cornerRadius="10dp"
app:currentValue="2"
app:delimiterColor="#color/white"
app:isAllRadius="true"
app:layout_constraintBottom_toTopOf="#+id/appCompatButton6"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:maxValue="4"
app:progressColor="#color/color_progress"
app:pyramidViewEnable="true"
app:step="1"
app:touchDisabled="false" />
<ImageView
android:layout_width="100dp"
android:layout_height="0dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
app:layout_constraintBottom_toTopOf="#+id/linearLayout"
app:layout_constraintEnd_toEndOf="#+id/linearLayout"
app:layout_constraintStart_toStartOf="#+id/linearLayout"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="#+id/linearLayout"
android:layout_width="0dp"
android:layout_height="30dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="15dp"
android:layout_marginBottom="5dp"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#+id/svsLevelView">
<ImageButton
android:id="#+id/appCompatButton"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:background="#color/white"
android:src="#drawable/laughing_emoji_svgrepo_com" />
<ImageButton
android:id="#+id/appCompatButton1"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:background="#color/white"
android:src="#drawable/scare_svgrepo_com" />
<ImageButton
android:id="#+id/appCompatButton2"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:background="#color/white"
android:src="#drawable/crying_emoji_svgrepo_com" />
<ImageButton
android:id="#+id/appCompatButton3"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:background="#color/white"
android:src="#drawable/sick_svgrepo_com" />
<ImageButton
android:id="#+id/appCompatButton4"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:background="#color/white"
android:src="#drawable/shocked_emoji_svgrepo_com" />
<ImageButton
android:id="#+id/appCompatButton5"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:layout_weight="1"
android:background="#color/white"
android:src="#drawable/angry_svgrepo_com" />
</LinearLayout>
<ImageButton
android:id="#+id/appCompatButton6"
android:layout_width="50dp"
android:layout_height="30dp"
android:layout_gravity="center"
android:layout_weight="1"
android:background="#color/white"
android:src="#drawable/ic_baseline_subdirectory_arrow_left_24"
app:layout_constraintBottom_toBottomOf="#+id/linearLayout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="#+id/linearLayout" />
</androidx.constraintlayout.widget.ConstraintLayout>
I appreciate you guys if u help me
sorry for my bad english
You're overriding onLayout but there's no code in it. So when the view needs to lay itself out... it doesn't! Remove the onLayout function and it should work.
Also, don't call inflate in that init function. You already inflated the layout and added it to your custom view here:
private val binding = ItemStickerBinding.inflate(LayoutInflater.from(context),this,true)
If you inflate again, you create a new layout and replace the old one. binding was created from the old layout, and all its references point to the old views.
You also don't want this, it's a duplicate layout:
<include layout="#layout/item_sticker"/>
Just as an example of how I'd write this class with view binding - maybe it helps! It should be easier to work with anyway:
// subclass FrameLayout instead - you're inflating a layout and putting it -inside-
// your custom view, so a FrameLayout (which is meant to hold a single view) is better
// and safer than a ConstraintLayout (which you're not providing constraints for -
// it could break at some point, or act weird)
// I've removed the View.OnClickListener interface because I'm setting the listeners
// another way
class StickerView #JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet?=null,
defStyle:Int = 0
) : FrameLayout(context,attributeSet,defStyle) {
// this inflates your layout and adds it to the view - job done!
private val binding =
ItemStickerBinding.inflate(LayoutInflater.from(context),this,true)
// If you need to refer to a view you've bound, you usually do it through
// the binding object - so "binding.appCompatButton0" etc, and there are ways
// to avoid saying "binding" all the time (see the init block below).
// But if you really want aliases, you could do it this way:
val btn0: ImageButton get() = binding.appCompatButton
init {
// this lets you avoid saying binding.this, binding.that - it can look neater
with(binding) {
// set a single click listener on multiple views
listOf(
appCompatButton, appCompatButton1, appCompatButton2,
appCompatButton3, appCompatButton4, appCompatButton5, appCompatButton6
).forEach { button ->
// call our onClick function with the clicked view
button.setOnClickListener { view -> onClick(view)}
// or setOnClickListener(::onClick)
}
// or you could set each button's action here, like this
appCompatButton1.setOnClickListener { doThing() }
appCompatButton2.setOnClickListener { doSomethingElse() }
}
}
// handle all your button clicks in a function if you want -
// v doesn't need to be nullable
private fun onClick(v: View) {
// because you're using view binding, you have references to all the views -
// so you don't need to use IDs at all
when (v) {
binding.appCompatButton -> { doThing() }
}
}
Personally, if you want the buttons to be named btn0 etc, I'd rename their IDs from appCompatButton to btn0 - that way you can refer to them as binding.btn0, if that's how you prefer to think about it! Give them the names you want to use.
Hope that helps!
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:
I am setting up View Binding in my project, and I ran into an issue. I tried to look for documentation regarding this under without luck.
I have my activity, and I have set up View Binding for this:
class AeropressActivity : AppCompatActivity() {
private lateinit var binding: ActivityAeropressBinding
private lateinit var bottomSheetBehavior: BottomSheetBehavior<ConstraintLayout>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAeropressBinding.inflate(layoutInflater)
setContentView(binding.root)
setupBottomSheet()
onClickListeners()
}
private fun setupBottomSheet() {
bottomSheetBehavior = BottomSheetBehavior.from(findViewById(R.id.layout_bottom_sheet))
// OnClickListener for bottomSheetBehavior
bottomSheetBehavior.addBottomSheetCallback(
object : BottomSheetBehavior.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {
}
override fun onStateChanged(bottomSheet: View, newState: Int) {
}
}
)
}
After setting this up, I want to use View Binding for my BottomSheet, as it has some buttons and a Chronometer that I want to add onClickListeners to.
I have added the
lateinit var bindingBottomSheet: LayoutBottomSheetBinding
but inflating this does nothing:
bindingBottomSheet = LayoutBottomSheetBinding.inflate(layoutInflater)
What am I missing to get this to work? Is this even possible?
Edit:
The BottomSheet layout looks like this:
<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/layout_bottom_sheet"
android:layout_width="match_parent"
android:layout_height="140dp"
app:behavior_peekHeight="50dp"
tools:context=".BottomSheetActivity"
app:behavior_hideable="false"
android:background="#drawable/background_bottom_bar"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<ImageView
android:id="#+id/image_view_button_home"
android:src="#drawable/ic_home"
android:layout_width="40dp"
android:layout_height="35dp"
android:layout_marginStart="24dp"
android:layout_marginTop="10dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="#+id/image_view_button_timer"
android:layout_width="40dp"
android:layout_height="35dp"
android:layout_marginTop="10dp"
android:src="#drawable/ic_stopwatch"
app:layout_constraintLeft_toRightOf="#id/image_view_button_home"
app:layout_constraintRight_toLeftOf="#id/image_view_button_info"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:src="#drawable/ic_info"
android:id="#+id/image_view_button_info"
android:layout_width="40dp"
android:layout_height="35dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintLeft_toRightOf="#id/image_view_button_timer"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/button_bottom_reset"
android:layout_width="100dp"
android:layout_height="50dp"
android:text="Reset"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/image_view_button_info" />
<Chronometer
android:gravity="center_horizontal"
android:textSize="48sp"
android:textColor="#color/white"
android:format="%s"
android:id="#+id/chronometer_bottom_bar"
android:layout_width="0dp"
android:paddingStart="6dp"
android:paddingEnd="6dp"
android:layout_height="60dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="#id/button_bottom_start"
app:layout_constraintStart_toEndOf="#id/button_bottom_reset"
app:layout_constraintTop_toBottomOf="#id/image_view_button_timer" />
<Button
android:id="#+id/button_bottom_start"
android:layout_width="100dp"
android:layout_height="40dp"
android:text="Start"
app:layout_constraintBottom_toTopOf="#id/button_bottom_stop"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#id/image_view_button_home" />
<Button
android:id="#+id/button_bottom_stop"
android:layout_width="100dp"
android:layout_height="40dp"
android:text="Stop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#id/button_bottom_start" />
</androidx.constraintlayout.widget.ConstraintLayout>
The BottomSheet has its own layout which is added to the activity's layout by using the .
I want to use View Binding for my BottomSheet, as it has some buttons and a Chronometer that I want to add onClickListeners to.
So, you can't access the underlying views of the BottomSheet from the AeropressActivity
First make sure that the BottomSheet layout is wrapped in <layout></layout> tag.
Then make sure to have an id to the <include> so that it allows you access the BottomSheet layout using data binding.
AeropressActivity layout:
<layout>
.
.
<include
android:id="#+id/bottom_sheet"
layout="#layout/bottom_sheet" />
</layout>
Then to access buttons in the BottomSheet layout:
bindingBottomSheet.bottomSheet.myButtonId
Assuming that the button id is: my_button_id
I am displaying a carousel within a RecyclerView. I implemented this carousel using ViewPager2. So the requirement is that the carousel should have no padding/margins and thus extend all the way to edge of the screen. However when displaying the first or last item, there should be a bit of whitespace to the left or right respectively. I've achieved that requirement using ItemDecoration applied to the ViewPager2:
class CustomItemDecoration(
private val leftMargin: Int,
private val rightMargin: Int,
private val firstLastMargin: Int,
) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect, view: View,
parent: RecyclerView, state: RecyclerView.State
) {
val position = parent.getChildAdapterPosition(view)
with(outRect) {
right = rightMargin
left = leftMargin
if (position == 0) {
left += firstLastMargin
} else if (position == state.itemCount - 1) {
right += firstLastMargin
}
}
}
}
However, this has caused the first and last item layouts suddenly having extra whitespace at the bottom. I do not understand why this is the case. Please see the following picture of the first item and the second item, note the whitespace present in the first item that is encircled:
My item layout looks like this:
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="#+id/image"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
I have seen this question and while it's similar, I do not understand how the accepted answer fixes the issue? How to resolve this issue?
just use android:scaleType in AppCompatImageView
<androidx.appcompat.widget.AppCompatImageView
android:id="#+id/image"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
to
<androidx.appcompat.widget.AppCompatImageView
android:id="#+id/image"
android:layout_width="0dp"
android:layout_height="0dp"
android:src="#drawable/splash_screen"
android:scaleType="fitXY"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
//other wise android:scaleType="center"
I'm adding new fragments dynamically to an existing fragment and I'm doing some calculation to infer the size of the new fragment.
After getting the new size i change the layout params of that new fragment to match the calculated size and this is where all the child views disappear - if i will not change the size the view will be presented perfectly ok.
XML code for the generated fragment:
<?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/playerCard"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#339CEF"
android:clickable="true"
android:focusable="true"
tools:context=".playerCard">
<ImageView
android:id="#+id/playerImage"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="20dp"
android:layout_marginTop="40dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="20dp"
android:background="#DA4444"
android:clickable="false"
android:contentDescription="TODO"
android:focusable="true"
android:visibility="visible"
app:layout_constraintBottom_toTopOf="#id/textView4"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.7"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0"
app:srcCompat="#drawable/ic_menu_camera"
tools:srcCompat="#drawable/ic_menu_camera" />
<TextView
android:id="#+id/textView4"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="20dp"
android:background="#color/teal_200"
android:clickable="false"
android:focusable="true"
android:text="TextView"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/playerImage" />
<ImageView
android:id="#+id/imageView3"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#99A9D8"
android:clickable="false"
android:contentDescription="TODO"
android:focusable="true"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0" />
</androidx.constraintlayout.widget.ConstraintLayout>
This is how i change the size on the card onCreateView (sizes were checked and are ok - not zero):
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState)
var view = inflater.inflate(R.layout.fragment_player_card, container, false)
view.x = this.centerPoint?.x?.toFloat() ?: 0.0f
view.y = this.centerPoint?.y?.toFloat() ?: 0.0f
view.layoutParams.width = this.cardSize?.width ?: 0
view.layoutParams.height = this.cardSize?.height ?: 0
view.requestLayout()
view.setOnTouchListener { v, event ->
onCardTouch(event)
}
return view
}
Also tried to do the size changing in a post statement but got the same results.
Any help will be appreciated.
Please use this solution here:
#Override
public void onResume() {
super.onResume();
getDialog().getWindow().setAttributes((android.view.WindowManager.LayoutParams) getLayoutParameters());
}
private final ViewGroup.LayoutParams getLayoutParameters(){
ViewGroup.LayoutParams layoutParameters = getDialog().getWindow().getAttributes();
layoutParameters.width = MYCALCULATEDSIZE.Width;
layoutParameters.height = MYCALCULATEDSIZE.Height;
return layoutParameters;
}
It's important to make sure that the layout parameters are updated when the Fragment Resumes, otherwise you could risk an exception, where the view has not been inflated yet.
Writing it here, makes sure that the fragment, along with its child views have been inflated properly.