In Android UI, we can create a custom view by overloading View as shown below.
class CustomView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0)
: View(context, attrs, defStyleAttr) {
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Perform the needing drawing
if (isAttachedToWindow) invalidate()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val desiredWidth = suggestedMinimumWidth + paddingLeft + paddingRight
val desiredHeight = suggestedMinimumHeight + paddingTop + paddingBottom
setMeasuredDimension(View.resolveSize(desiredWidth, widthMeasureSpec),
View.resolveSize(desiredHeight, heightMeasureSpec))
}
}
Can we wrap JetpackCompose in this CustomView, so that the underlying Drawing it using JetpackCompose instead?
I check https://developer.android.com/jetpack/compose/interop/interop-apis, don't seems to have it stated.
To get it to work, we can have
class CustomComposeView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AbstractComposeView(context, attrs, defStyleAttr) {
#Composable
override fun Content() {
// JetpacCompose code here
}
}
In the XML, we can have something like this
<com.package.CustomComposeView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="700dp" />
Related
I want to have an imageView that is fully visible at the top, but further down it fades out, resulting in being fully transparent in the bottom. How do I achieve this?
I have a solution for this, but it is not perfect
class LinearGradientView : View {
var bitmap: Bitmap
private val paint = Paint()
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
setMeasuredDimension(MeasureSpec.makeMeasureSpec(bitmap.width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(bitmap.height, MeasureSpec.EXACTLY))
}
override fun onDraw(canvas: Canvas) {
val shaderA: Shader = LinearGradient(0F, 0F, 0F, bitmap.height.toFloat(), -0x1, 0x00ffffff, Shader.TileMode.CLAMP)
val shaderB: Shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
paint.shader = ComposeShader(shaderA, shaderB, PorterDuff.Mode.SRC_IN)
canvas.drawRect(0f, 0f, bitmap.width.toFloat(), bitmap.height.toFloat(), paint)
}
init {
bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.ktor)
}
}
The key is to leverage ComposeShader with LinearGradient and BitmapShader.
reference:
Android extends imageView gradient transparent vertical linear from input bitmap
Drawing a Bitmap to a Canvas with an alpha gradient
I am trying to set up a canvas to redraw whenever the user clicks a button. Whenever I trigger the redraw via invalidate() the app freezes and crashes. I am very new to android development so I am struggling to see what I neglecting. Any insight would be appriciated.
The relevent classes are included below.
Thanks,
mainactivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
fun submit(view: View) {
val text = findViewById<View>(R.id.editTextNumberDecimal) as EditText
val canvas = findViewById<View>(R.id.customCanvas) as CustomCanvas
val value = text.text.toString()
val t = findViewById<View>(R.id.textView) as TextView
t.text = value
canvas.setWeightandRedraw(value.toFloat())
}
}
canvas
class CustomCanvas : View {
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
}
var weight: Float = 0.0f;
override fun onDraw(canvas: Canvas?) {
drawBar(canvas);
drawWeightKg(canvas, this.weight)
}
fun setWeightandRedraw(_weight: Float) {
weight = _weight;
invalidate();
}
fun drawBar(canvas: Canvas?) {
.........
}
fun drawWeightKg(canvas: Canvas?, weight: Float) {
.........
}
}
Moving invalidate() to onDraw() appeard to fix this.
override fun onDraw(canvas: Canvas?) {
drawBar(canvas);
drawWeightKg(canvas, this.weight)
invalidate();
}
I have a Custom EditTextClass that Im using for all EditTexts on my App -
Class CustomEditText : EditText {
...
}
I want to change the background image, so that it should be reflected in all the usages of this CustomEditText.
I tried this -
override fun onDraw(canvas: Canvas ? ) {
val d = AppCompatResources.getDrawable(mContext!!, R.drawable.new_drawable)
d?.draw(canvas!!)
super.onDraw(canvas)
}
and this -
fun init(context: Context ? , attrs : AttributeSet ? ) {
background = ResourcesCompat.getDrawable(context!!.getResources(), R.drawable.new_drawable, null)
setBackground(background)
}
Both the ways, don't work. Can anyone tell me what is the correct solution ?
You need to setBounds to drawable when draw drawable.
So you should make your draw method like this.
override fun onDraw(canvas: Canvas? ) {
val d = AppCompatResources.getDrawable(context!!, R.drawable.new_drawable)
d?.setBounds(0, 0, width, height)
d?.draw(canvas!!)
super.onDraw(canvas)
}
First draw to canvas and pass the canvas to parent meaning super.onDraw should be at bottom
override fun onDraw(canvas: Canvas ? ) {
val d = AppCompatResources.getDrawable(mContext!!, R.drawable.new_drawable)
d?.draw(canvas!!)
super.onDraw(canvas)
}
In my opinion, you should not override onDraw method as this could cause issues when super.onDraw(canvas) is called before your operations (your drawable could cover the text, hits, drawable elements, etc.).
I've made a demo, which worked for me:
class CustomEditText : AppCompatEditText {
constructor(context: Context) : super(context) {
init()
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init()
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context,
attrs,
defStyleAttr) {
init()
}
private fun init() {
setBackgroundResource(R.drawable.new_drawable)
}
}
I'm a library author and have to intercept all touch events of child views, by overriding ViewGroup.onInterceptTouchEvent().
First I wrote the following code (simplified):
interface touchIntercepter {
// my library set this field to intercept touch event
var touchHandler: ((MotionEvent) -> Boolean)?
}
class LinearLayoutTouchIntercepter #JvmOverloads constructor (
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
)
: touchIntercepter
, LinearLayout(context, attrs, defStyleAttr, defStyleRes)
{
override var touchHandler: ((MotionEvent) -> Boolean)? = null
override fun onInterceptTouchEvent(event: MotionEvent) = touchHandler?.invoke(event) ?: false
}
Library users can use the LinearLayoutTouchInterceptor in their layout xml file instead of standard LinearLayout and then my library code can intercept touch event of the user layout's child views by touchIntercepter interface.
I think it is wonderful if there's something like ViewGroup.setOnInterceptTouchListener(), like View.setOnClickListener(), but I found that there isn't.
Now the problem is, I want to provide the same functionality for RelativeLayout, FrameLayout and other ViewGroup descendants.
For example,
class RelativeLayoutTouchIntercepter #JvmOverloads constructor (
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
)
: touchIntercepter
, RelativeLayout(context, attrs, defStyleAttr, defStyleRes)
{
override var touchHandler: ((MotionEvent) -> Boolean)? = null
override fun onInterceptTouchEvent(event: MotionEvent) = touchHandler?.invoke(event) ?: false
}
As you can see, all code is the same but the only difference is inheriting XXXXXLayout instead of LinearLayout. I don't want to copy and paste them but have no idea how to reduce the duplication.
It seems that Kotlin generics are not helping in this case while C++ template perfectly can help like this pseudo code :
template <typename T>
class TouchInterceptorTmpl : public T
{
void onInterceptTouchEvent() override;
};
using RelativeLayoutTouchInterceptor = TouchInterceptorTmpl<RelativeLayout>;
using FrameLayoutTouchInterceptor = TouchInterceptorTmpl<FrameLayout>;
No way to do like this in Kotlin?
You can reduce duplication a little bit by making a concrete implementation of your interface and using it as a delegate. Unfortunately, you can't avoid overriding onInterceptTouchEvent in each implementation due to how inheritance works, but you can make an extension function for your interface to shorten that code a bit.
Note, interface names in Kotlin are capitalized by convention.
Setup:
interface TouchInterceptor {
var touchInterceptionHandler: ((MotionEvent) -> Boolean)?
}
class TouchInterceptorImpl: TouchInterceptor {
override var touchInterceptionHandler: ((MotionEvent) -> Boolean)? = null
}
fun TouchInterceptor.intercept(event: MotionEvent): Boolean = touchInterceptionHandler?.invoke(event) ?: false
Usage:
class RelativeLayoutTouchIntercepter #JvmOverloads constructor (
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
)
: TouchInterceptor by TouchInterceptorImpl()
, RelativeLayout(context, attrs, defStyleAttr, defStyleRes)
{
override fun onInterceptTouchEvent(event: MotionEvent): Boolean = intercept(event)
}
I am extending a RelativeLayout to make a fragment decorator.
Like this:
<br.com.simplepass.mapfragmentwrapper.MapFragmentWrapper
android:id="#+id/map_wrapper"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="#+id/main_fragment_map"
android:layout_width="match_parent"
android:layout_height="match_parent"
class="com.google.android.gms.maps.MapFragment"/>
</br.com.simplepass.mapfragmentwrapper.MapFragmentWrapper>
Well, I would like to add some images in the RelativeLayout so it always get on top of the fragment, so I did this:
class MapFragmentWrapper : RelativeLayout {
var mMarkImageView : ImageView? = null
var mShadowView : View? = null
constructor(context: Context) : super(context) {
init(context)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(context)
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(context)
}
#TargetApi(23)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
init(context)
}
private fun init(context: Context) {
val params = RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT)
params.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
mMarkImageView = ImageView(context)
mMarkImageView?.setImageResource(R.drawable.ic_marker_centered)
mShadowView = View(context)
mShadowView?.setBackgroundResource(R.drawable.map_pin_shadow)
addView(mMarkImageView, params)
addView(mShadowView, params)
}
[some more code here...]
}
But, when my activity starts, the fragment gets on top of my added views (mMarkImageView and mShadowView)... and that's exactly the opposite of what a want.
So how do I programmatically put the views at the last position in viewgroups?
Any help is appreciated!
So I found the answer:
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
super.onLayout(changed, l, t, r, b)
removeView(mMarkImageView)
removeView(mShadowView)
addView(mMarkImageView, -1 , params)
addView(mShadowView, -1, params)
}
doing this I put the layouts at the end of the viewgroup