Sometimes I have a button in my UI that it is so small that it is difficult to click. My solution so far has been to add a transparent border around the button in photoshop. Just increasing the padding on the button does not work, since this will also stretch the image. Since it is kind of a fuss to open photoshop each time I want to change the clickable surface, is there any way to do this programmatically? I have tried placing a framelayout behind the button and make it clickable, but then the button wont change appearance on touch as it should. Ofcourse I could also add a ontouchlistener on the framelayout which changes the buttons appearance, but then it quite some code if I have several of those buttons.
Cheers,
Me personally, I'd use a TouchDelegate. This lets you deal with the touch target, and the visual view bounds as two different things. Very handy...
http://developer.android.com/reference/android/view/TouchDelegate.html
I have just found a neat way to solve this problem.
Surround the button with a say a LinearLayout that has the padding round the button.
Add the same onclick to the LinearLayout as the Button.
In the Button set the duplicateParentState to true which make the button highlight when you click outside the button but inside the LinearLayout.
<LinearLayout
android:layout_height="fill_parent"
android:onClick="searchButtonClicked"
android:layout_centerVertical="true"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:paddingRight="10dp"
android:paddingLeft="30dp">
<Button
android:id="#+id/search_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#drawable/toggle_button_selector"
android:textColor="#fff"
android:text="Search"
android:focusable="true"
android:textStyle="bold"
android:onClick="searchButtonClicked"
android:layout_gravity="center_vertical"
android:duplicateParentState="true"/>
</LinearLayout>
This is a very late "me too," but after coming to this and other questions looking for a solution, I found a simple, elegant solution of my own.
Another question complained that the transparent background of their image was not clickable. If that is an issue, this seems to get around that as well.
Here's the button:
<ImageButton
android:id="#+id/arrowUp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/arrow_up"
android:background="#drawable/clear_button_background" />
The relevant lines are the last two. "#drawable/arrow_up" is a few button states in *.png files defined in a drawable *.xml file like this:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_pressed="true"
android:drawable="#drawable/tri_up_blue" /> <!-- pressed -->
<item android:state_selected="true"
android:drawable="#drawable/tri_up_blue" /> <!-- selected -->
<item android:state_focused="true"
android:drawable="#drawable/tri_up_blue" /> <!-- focused -->
<item android:drawable="#drawable/tri_up_green" /> <!-- default -->
</selector>
Just your basic button. Nothing special. And "#drawable/clear_button_background" is just this:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#android:color/transparent"/>
<size android:width="20dp" android:height="30dp" />
</shape>
The height and width here are the clickable area, resize as needed. You can reuse this for as many buttons as you need in a single view, unlike the absurdly detailed TouchDelegate. No additional listeners. It doesn't add any views or groups to your hierarchy and you won't be messing around with padding and margins all day.
Simple. Elegant.
I think your solution is the best one available at the moment, if you don't want to go deep into some android stuff and intercept all the motionEvent and TouchEvents yourself and then you also would need to trigger the pressed view of the button yourself etc.
Just create a nine patch image with a stretchable transparent border. In that way you can change the size of the image without the need to change the image itself and your button will grow or shrink without the actual displayed background changing.
Anothe idea is to make the new transparent image and put icon on it so the touch area will be more and design look perfect. Check out the image
Thank you.
Simply provide padding to the layout in place of Margin
<ImageView
android:id="#+id/back_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="25dip"
android:src="#drawable/back_btn" />
What I did is not the recommended way and very few will find it useful.
For me it was the best solution, because TouchDelegate did not worked in all View hierarchy scenarios.
The solution is based on overriding the hidden
View class
public boolean pointInView(float localX, float localY, float slop)
Here is the code:
public boolean pointInView(float localX, float localY, float slop) {
boolean res = ae_pointInView(localX, localY, slop, slop);
if(!res) {
if(viewIsClickableButToSmall(this)) {
//our view is clickable itself
float newSlopX = Math.max(slop, slop + minTouchableViewWidth - this.getWidth());
float newSlopY = Math.max(slop, slop + minTouchableViewHeight - this.getHeight());
if(ae_pointInView(localX, localY, newSlopX, newSlopY)) {
return true;
}
}
//the point is outside our view, now check for views with increased tap area that may extent beyond it
int childCount = getChildCount();
for(int i = 0; i < childCount;i++) {
View child = getChildAt(i);
if(child instanceof MyViewGroup) {
//this is our container that may also contain views with increased tap area
float[] newPoint = ae_transformPointToViewLocal(localX, localY, child);
if(((MyViewGroup) child).pointInView(newPoint[0], newPoint[1], slop)) {
return true;
}
}
else {
if(viewIsClickableButToSmall(child)) {
float[] newPoint = ae_transformPointToViewLocal(localX, localY, child);
float newSlopX = Math.max(slop, slop + minTouchableViewWidth - this.getWidth());
float newSlopY = Math.max(slop, slop + minTouchableViewHeight - this.getHeight());
if(ae_pointInView(newPoint[0], newPoint[1], newSlopX, newSlopY)) {
return true;
}
}
}
}
return false;
}
else
return true;
}
private int minTouchableViewHeight, minTouchableViewWidth;
private boolean viewIsClickableButToSmall(View view)
{
minTouchableViewHeight = GlobalHelper.dipToDevicePixels(40);
minTouchableViewWidth = GlobalHelper.dipToDevicePixels(40);
return (view.getHeight() < minTouchableViewHeight || view.getWidth() < minTouchableViewWidth) && view.hasOnClickListeners();
}
public boolean ae_pointInView(float localX, float localY, float slopX, float slopY) {
boolean res = localX >= -slopX && localY >= -slopY && localX < ((getRight() - getLeft()) + slopX) &&
localY < ((getBottom() - getTop()) + slopY);
return res;
}
private float[] tempPoint;
public float[] ae_transformPointToViewLocal(float x, float y, View child) {
if(tempPoint == null) {
tempPoint = new float[2];
}
tempPoint[0] = x + getScrollX() - child.getLeft();
tempPoint[1] = y + getScrollY() - child.getTop();
return tempPoint;
}
Maybe you could do so by looking at the X and Y coordinates of the MotionEvent passed into onTouchEvent ?
Related
Currently, I need to use paddingTop and paddingBottom of RecyclerView, as I want to avoid complex space calculation, in my first RecyclerView item and last item.
However, I notice that, requiresFadingEdge effect will be affected as well.
This is my XML
<androidx.recyclerview.widget.RecyclerView
android:requiresFadingEdge="vertical"
android:paddingTop="0dp"
android:paddingBottom="0dp"
android:overScrollMode="always"
android:background="?attr/recyclerViewBackground"
android:id="#+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false" />
When paddingTop and paddingBottom is 40dp
As you can see, the fading effect shift down by 40dp, which is not what I want.
When paddingTop and paddingBottom is 0dp
Fading effect looks fine. But, I need to have non-zero paddingTop and paddingBottom, for my RecyclerView.
Is there a way to make RecyclerView's requiresFadingEdge unaffected by paddingTop and paddingBottom?
I found the best and kind of official solution for this. Override this 3 methods of RecyclerView. Which play most important role for fading edge.
First create your recyclerView.
public class MyRecyclerView extends RecyclerView {
// ... required constructor
#Override
protected boolean isPaddingOffsetRequired() {
return true;
}
#Override
protected int getTopPaddingOffset() {
return -getPaddingTop();
}
#Override
protected int getBottomPaddingOffset() {
return getPaddingBottom();
}
}
That's it. Use this recyclerview and you will see fadding edge unaffected.
Following content is just for explanation. If you want to know behind the scene.
To understand how this edge effect is working I dig into the class where android:requiresFadingEdge is used, And I found that it's not handled by RecyclerView instead It's handled by View class which is parent for all view.
In onDraw method of View class I found the code for drawing fade edge by using help of this method isPaddingOffsetRequired. Which used only for handling the fade effect.
According to documentation this method should be overridden by child class If you want to change the behaviour of fading edge. Bydefault It return false. So By returning true we are asking view to apply some offset for edge at the time of view drawing.
Look following snippet of onDraw method of View class to understand the calculation.
final boolean offsetRequired = isPaddingOffsetRequired();
if (offsetRequired) {
paddingLeft += getLeftPaddingOffset();
}
int left = mScrollX + paddingLeft;
int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
int top = mScrollY + getFadeTop(offsetRequired);
int bottom = top + getFadeHeight(offsetRequired);
if (offsetRequired) {
right += getRightPaddingOffset();
bottom += getBottomPaddingOffset();
}
As we can see top variable is initialize using getFadeTop(offsetRequired).
protected int getFadeTop(boolean offsetRequired) {
int top = mPaddingTop;
if (offsetRequired) top += getTopPaddingOffset();
return top;
}
In this method, top is calculated by adding value of topOffSet when offset is needed. So to reverse the effect we need to pass negative value of padding which you are passing. so we need to return -getPaddingTop().
Now for bottom we are not passing negative value because bottom is working on top + height. So passing negative value make fade more shorter from the bottom so we need to add bottom padding to make it proper visible.
You can override this 4 method to play with it. getLeftPaddingOffset(), getRightPaddingOffset(), getTopPaddingOffset(), getBottomPaddingOffset()
I suggest a few solutions
, I hope to be helpful
1- RecyclerView.ItemDecoration (keep requiresFadingEdge)
class ItemDecoration(private val spacing: Int) : RecyclerView.ItemDecoration(){
override fun getItemOffsets(outRect: Rect, view: View, parent:
RecyclerView, state: RecyclerView.State?) {
val position = parent.getChildAdapterPosition(view)
when(position)
0 -> { outRect.top = spacing /*40dp*/ }
parent.adapter.itemCount-1 -> { outRect.bottom = spacing /*40dp*/ }
}
2- Multiple ViewHolders (depending on the ViewHolder either keep or remove requiresFadingEdge)
https://stackoverflow.com/a/26245463/5255963
3- Gradient (remove requiresFadingEdge)
Make two gradients with drawables like this:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:type="linear"
android:angle="90"
android:startColor="#000"
android:endColor="#FFF" />
</shape>
And set background to two views at the top and bottom of the Recyclerview.
There are many ways to achieve that but I preferred below solution that works for me.
You can use a third-party library called Android-FadingEdgeLayout checkout here
Here is a dependency.
implementation 'com.github.bosphere.android-fadingedgelayout:fadingedgelayout:1.0.0'
In yours.xml
<com.bosphere.fadingedgelayout.FadingEdgeLayout
android:id="#+id/fading_edge_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:fel_edge="top|left|bottom|right"
app:fel_size_top="40dp"
app:fel_size_bottom="40dp"
app:fel_size_left="0dp"
app:fel_size_right="0dp">
<androidx.recyclerview.widget.RecyclerView
android:paddingTop="0dp"
android:paddingBottom="0dp"
android:overScrollMode="always"
android:id="#+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false" />
</com.bosphere.fadingedgelayout.FadingEdgeLayout>
Change your ReacyclerView property according to your need. below is example image with all sided sades. I hope that will help you.
Credits Android-FadingEdgeLayout
it is almost 6 days traying to move the layout, but no success at all, trying alot of helps from internet but none helps..
I want llPic1 and llPic2 to be moved above ivNjeri with OnTouchListener.
So player to dicide which one need to be moved above ivNjeri.
With this code it vibrates on move and llPic1 and llPic2 goes under ivNjeri:
float dx = 0, dy = 0, x = 0, y = 0;
#Override
public boolean onTouch(View view, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
x = event.getX();
y = event.getY();
dx = x - view.getX();
dy = y - view.getY();
}
break;
case MotionEvent.ACTION_MOVE: {
view.setX(event.getX() - dx);
view.setY(event.getY() - dy);
}
break;
}
return true;
}
I'm also trying alot of other codes but none works, any help will be very very appriciated :)
You should use only 1 ViewGroup which holds all the images including an image to be overlapped and images to move.
Why image cannot go over the big image?
The small image on right side is inside of nested LinearLayout which is restricting the small image to be within the LinearLayout. That is why you can move the image inside of the child LinearLayout but not go beyond the boundary.
One example to fix it using RelativeLayout and fixed width on big image:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<ImageView
android:id="#+id/bigimage"
android:layout_width="200dp"
android:layout_height="300dp"
android:src="#drawable/ic_launcher"/>
<ImageView
android:id="#+id/smallimage"
android:layout_width="match_parent"
android:layout_height="100dp"
android:src="#drawable/ic_launcher"
android:layout_toRightOf="#id/bigimage"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="100dp"
android:src="#drawable/ic_launcher"
android:layout_toRightOf="#id/bigimage"
android:layout_below="#id/smallimage"/>
</RelativeLayout>
If you want to keep the precise weight, check out
https://developer.android.com/reference/android/support/percent/PercentFrameLayout.html
https://developer.android.com/reference/android/support/percent/PercentRelativeLayout.html
This can give you weight control and can hold all views in 1 ViewGroup.
If you are trying only to animate view why you dont use Animation for it like this:
TranslateAnimation animation = new TranslateAnimation(0.0f, 400.0f,0.0f, 0.0f);
// new TranslateAnimation(xFrom,xTo, yFrom,yTo)
animation.setDuration(5000); // animation duration
animation.setRepeatCount(1); // animation repeat count
animation.setRepeatMode(1); // repeat animation (left to right, right to left )
//animation.setFillAfter(true);
llPic1.startAnimation(animation); // start animation
you can try this tutorial link. i think you need remove your pic 1 or pic2 (if you want disapeare after dragging.) and then you should add another image view on ivNjeri view
I have a relative layout containing three textviews, each having a width of half-width of the screen. I want the user to be able to use a scroll gesture and move these textviews together, and if the textview located far left goes off-screen, it is moved to the far right next to the third textview. So I want to create a sort of a endless scroller-system.
However, using the code below results in gaps between the views when scrolling, and I think the gap widths are dependable on the scrolling speed.
Here is a link to a screenshot of the problem: http://postimg.org/image/bnl0dqsgd/
Currently I have implemented scrolling only for one direction.
XML:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/rel_layout"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false">
<com.app.healthview.BorderedTextView
android:id="#+id/btvYear1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:background="#color/YearColor1"
android:maxLines="1"
android:text="2012" />
<com.app.healthview.BorderedTextView
android:id="#+id/btvYear2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_alignParentTop="true"
android:layout_toRightOf="#+id/btvYear1"
android:background="#color/YearColor2"
android:maxLines="1"
android:text="2013" />
<com.app.healthview.BorderedTextView
android:id="#+id/btvYear3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#color/YearColor1"
android:layout_alignParentTop="true"
android:layout_toRightOf="#+id/btvYear2"
android:gravity="center"
android:maxLines="1"
android:text="2014" />
</RelativeLayout>
Then I initialize the views in a function, which is called after setting the content view:
public void InitTimeView() {
year_views = new BorderedTextView[3];
year_views[0] = (BorderedTextView) findViewById(R.id.btvYear1);
year_views[1] = (BorderedTextView) findViewById(R.id.btvYear2);
year_views[2] = (BorderedTextView) findViewById(R.id.btvYear3);
// Acquire display size
display = getWindowManager().getDefaultDisplay();
size = new Point();
display.getSize(size);
int year_width = size.x / 2;
year_views[0].setWidth(year_width);
year_views[1].setWidth(year_width);
year_views[2].setWidth(year_width);
// This is done, because when scrolling, the third view which in the beginning is off-screen, could not be seen
RelativeLayout relLayout = (RelativeLayout) findViewById(R.id.rel_layout);
relLayout.getLayoutParams().width = year_width * 4;
relLayout.invalidate();
}
Then the onScroll-method:
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// intCurrYearMember is public, it stores the view that is next to be moved
// intRightYearMember; intCurrYearMember is located right to this view.
switch(intCurrYearMember) {
case 0:
intRightYearMember = 2;
case 1:
intRightYearMember = 0;
case 2:
intRightYearMember = 1;
}
// Move the views
for (TextView textview : year_views) {
textview.setX(textview.getX() - (distanceX / 2));
}
// Check if the view most left is now too far on left, and move it if needed to far right
if ((year_views[intCurrYearMember].getX() + year_views[intCurrYearMember].getWidth()) <= 0) {
// Is the problem here perhaps?
year_views[intCurrYearMember].setX(year_views[intRightYearMember].getRight());
intPreviousMember = intCurrYearMember;
if (intCurrYearMember < 2)
intCurrYearMember++;
else
intCurrYearMember = 0;
}
return true;
}
As it shows in the code, my idea is to build a year scroller. If someone happends to have a better, more efficient idea for how to do it, I am happy to hear your advices!
So my question is: why are there gaps between the textviews?
I would not suggest doing this at all. Utilizing Horizontal Swipes for these things is not a standard use of the platform and why waste your time developing this when there are already tested and pretty Android Components such as the Pickers that you can use easily.
A Horizontal Swiping gesture is normally saved for a Menu Drawer, View Pager, or other piece of functionality.
I want to make an Android app where you see an image of a Dutch map, there you can select an province.
Each province has to go to another class.
The best way i found was to do it with 2 images, 1 you displayed and the other one exactly the same but with colors. Than get the color with touchEvent and let say if its Red go to a class.
So far i've 2 images, one i displayed and the other one (exactly the same but each province have another color), this image i maked 'invisible'.
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="#ffffff" >
<ImageView
android:id="#+id/img_bg"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:scaleType="fitXY"
android:src="#drawable/nl_clickable_original" />
<ImageView
android:id="#+id/img_hitbox"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:scaleType="fitXY"
android:src="#drawable/nl_clickable"
android:visibility="invisible" />
</FrameLayout>
But now i have no idea how to go further.
I found some code on StackOverflow which should get the colors from the image but i don't no how to implement it.
private int getColour(int x, int y) {
ImageView img = (ImageView) findViewById(R.id.img_hitbox);
img.setDrawingCacheEnabled(true);
Bitmap hotspots = Bitmap.createBitmap(img.getDrawingCache());
img.setDrawingCacheEnabled(false);
return hotspots.getPixel(x, y);
}
Do i it the right way or have someone a better idea how to made this?
I've searching for 1 week now so a bit help would be nice :)!
Thanks
In the activity's onCreate, you can do something like (not tested)
ImageView img = (ImageView) findViewById(R.id.img_bg);
img.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP){
int x = (int) event.getX();
int y = (int) event.getY();
int colour = getColour( x, y);
//switch to correct province
}
return true;
}
});
I'm not sure though that getColour will work correctly for an invisible (or a hidden view) View. An alternative could be to do something like (again not tested)
private int getColour( int x, int y)
{
ImageView img = (ImageView) findViewById(R.id.img_bg);
Drawable d = getResources().getDrawable(R.drawable.nl_clickable);
Bitmap b1 =((BitmapDrawable)d).getBitmap();
//scale loaded bitmap to same resolution as visible view
Bitmap hotspots = Bitmap.createScaledBitmap(b1, img.getWidth(), img.getHeight(), false);
return hotspots.getPixel(x, y);
}
One image will be on top of the other, so you will never be able to click the one a the bottom.
If your map is not scrollable, it will be easy, with the ontouch event, just keep in mind that the screen can have different densities.
The main problem in your case is the screen densities. You could divide your image in to coordinates and then by using ontouch event do what you want to do, but as there are millions of different screen's density it will be difficult for you. To mention, that peace of code that brings your back a color is also depends on coordinates, you have to pass x and y coordinates of the image so then it will give you back colors, so forget about colors. Better think about screen densities, and do research on this way.
How I can to make custom circle button? (only make clicks on circle )
Is any way to make it with circle png file?
I tried with imageView override onTouch method but it works very badly because view.getWidth(), view.getHeight() and view.getTop... methods works very bad
public boolean inCircle(MotionEvent e, int radius, int x, int y) {
int dx = (int) (e.getX() - x);
int dy = (int) (e.getY() - y);
double d = Math.sqrt((dx * dx) + (dy * dy));
if (d < radius)
return true;
return false;
}
Thanks.
Its very simple. Create a custom shape drawable and set that as the background of your view. Example:
round_button_drawable.xml in drawable/
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval" >
<solid android:color="#android:color/holo_orange_dark"/>
</shape>
set this drawable as the background of any view.
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#drawable/round_button_drawable"
android:text="btn"
/>
Another way would be to extend Button and override its onDraw method
You can then draw a circle on the canvas using canvas.drawCircle method.
You can also draw the circle.png file on the canvas using Drawable.draw method
Dont go for complex logic just select any rounded image and set that image as a background of your simple button it will look like simple round button and it will accept click only on that round shape.
there is ImageButton class which can serve ur purpose..