I'm trying to create a custom view with the height bigger than the display's (or parent's) height. This view must be scrollable. I use onTouchEvent to measure new finger position, and I actually get what I want, but my view doesn't move. I've searched in the internet for the problem's solution, but didn't find a suitable answer.
This's my View
public class SmartScroll extends View{
public SmartScroll(Context context) {
super(context);
}
int count = 0;
int yStart = 0, yEnd = 0;
#Override
public boolean onTouchEvent(MotionEvent event) {
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
yStart = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
MeasureSpec.toString(getMeasuredHeight());
MeasureSpec.toString(getMeasuredWidth());
count++;
yEnd = (int) event.getY();
scrollBy(0, (yEnd - yStart));
yStart = yEnd;
break;
case MotionEvent.ACTION_UP:
yStart = 0;
yEnd = 0;
break;
}
return true;
}
and this's how I create it inActivity:
public class MyActivity extends Activity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SmartScroll scrollView = new SmartScroll(getApplicationContext()) {
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(1500, MeasureSpec.EXACTLY));
}
};
scrollView.setBackgroundDrawable(getResources().getDrawable(R.drawable.gradient));
setContentView(scrollView);
}
}
Please, help me to make it work
Unfortunately I didn't find the answer on my question. I tried some working examples from the internet which use scrollBy() or scrollTo() and they really work.
So, I found solution that works for me. As I couldn't move the content of my custom view, I decided to move the whole view.
I do this by listening to onTouchEvents, and setting new Y coordinates of my view. After setting new Y I call invalidate().
UPDATE:
I finally found the reason of my initial problem. It was in the way I created my custom ViewGroup. I set height of my ViewGroup bigger than the height of its parent and wanted to scroll its content. But content was the same size of the very ViewGroup, so scrollBy() didn't find the need to scroll content.
What I did was I filled my ViewGroup with children so that the content became bigger than the view, measured my ViewGroup to MatchParent and scrollBy() finally started working.
Related
I'm stuck on this problem in Android Drag and Drop. I've already read the documentation and more than one question/answer here on stackoverflow .
The problem is that:
i've 3 ImageView that can i move inside another 3 ImageView, but when i start to drag one of the 3 ImageView i can drop it inside in just one ImageView area.
Here the code! . Basically i'll see active only the dropViewArancio area even if i drag the other ImageView.
What i want it's to have all the 3 area active where i can drop one of the 3 ImageView.
Thanks for the support!
An ImageView can't contain another View inside, only classes derived from ViewGroup can. Then you can move an ImageView over or behind another (depending on Z order) but not inside.
Here a working example of how to move all the views contained in a RelativeLayout using onTouch. I think that onDrag event applies better to drag and drop data items, not to move views. Hope it will help
public class MainActivity extends AppCompatActivity implements View.OnTouchListener {
private RelativeLayout mRelLay;
private float mInitialX, mInitialY;
private int mInitialLeft, mInitialTop;
private View mMovingView = null;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRelLay = (RelativeLayout) findViewById(R.id.relativeLayout);
for (int i = 0; i < mRelLay.getChildCount(); i++)
mRelLay.getChildAt(i).setOnTouchListener(this);
}
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
RelativeLayout.LayoutParams mLayoutParams;
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
mMovingView = view;
mLayoutParams = (RelativeLayout.LayoutParams) mMovingView.getLayoutParams();
mInitialX = motionEvent.getRawX();
mInitialY = motionEvent.getRawY();
mInitialLeft = mLayoutParams.leftMargin;
mInitialTop = mLayoutParams.topMargin;
break;
case MotionEvent.ACTION_MOVE:
if (mMovingView != null) {
mLayoutParams = (RelativeLayout.LayoutParams) mMovingView.getLayoutParams();
mLayoutParams.leftMargin = (int) (mInitialLeft + motionEvent.getRawX() - mInitialX);
mLayoutParams.topMargin = (int) (mInitialTop + motionEvent.getRawY() - mInitialY);
mMovingView.setLayoutParams(mLayoutParams);
}
break;
case MotionEvent.ACTION_UP:
mMovingView = null;
break;
}
return true;
}
}
So, i've solved my problem following this tutorial. Anyway my desire solution it's something similar this, the perfect way it's a mixing of tutorial and a previous link. Thanks all for help.
I have some problem trying to control touch event propagation within my RecycleView. So I have a RecyclerView populating a set of CardViews imitating a card stack (so they overlap each other with a certain displacement, though they have different elevation value). My current problem is that each card has a button and since relative card displacement is smaller than height of the button it results in the situation that buttons are overlapping and whenever a touch event is dispatched it starts propagating from the bottom of view hierarchy (from children with highest child number).
According to articles I read (this, this and also this video) touch propagation is dependent on the order of views in parent view, so touch will first be delivered to the child with highest index, while I want the touch event to be processed only by touchables of the topmost view and RecyclerView (it also has to process drag and fling gestures). Currently I am using a fallback with cards that are aware of their position within parent's view hierarchy to prevent wrong children from processing touches but this is really ugly way to do that. My assumption is that I have to override dispatchTouchEvent method of the RecyclerView and properly dispatch a touch event only to topmost child. However, when I tried this way of doing that (which is also kind of clumsy):
#Override
public boolean dispatchTouchEvent(MotionEvent ev) {
View topChild = getChildAt(0);
for (View touchable : topChild.getTouchables()) {
int[] location = new int[2];
touchable.getLocationOnScreen(location);
RectF touchableRect = new RectF(location[0],
location[1],
location[0] + touchable.getWidth(),
location[1] + touchable.getHeight());
if (touchableRect.contains(ev.getRawX(), ev.getRawY())) {
return touchable.dispatchTouchEvent(ev);
}
}
return onTouchEvent(ev);
}
Only DOWN event was delivered to the button within a card (no click event triggered). I will appreciate any advice on the way of reversing touch event propagation order or on delegating of touch event to a specific View. Thank you very much in advance.
EDIT: This is the screenshot of how the example card stack is looking like
Example adapter code:
public class TestAdapter extends RecyclerView.Adapter {
List<Integer> items;
public TestAdapter(List<Integer> items) {
this.items = items;
}
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.test_layout, parent, false);
return new TestHolder(v);
}
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
TestHolder currentHolder = (TestHolder) holder;
currentHolder.number.setText(Integer.toString(position));
currentHolder.tv.setTag(position);
currentHolder.tv.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
int pos = (int) v.getTag();
Toast.makeText(v.getContext(), Integer.toString(pos) + "clicked", Toast.LENGTH_SHORT).show();
}
});
}
#Override
public int getItemCount() {
return items.size();
}
private class TestHolder extends RecyclerView.ViewHolder {
private TextView tv;
private TextView number;
public TestHolder(View itemView) {
super(itemView);
tv = (TextView) itemView.findViewById(R.id.click);
number = (TextView) itemView.findViewById(R.id.number);
}
}
}
and an example card layout:
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_margin="16dp"
android:textSize="24sp"
android:id="#+id/number"/>
<TextView android:layout_width="wrap_content"
android:layout_height="64dp"
android:gravity="center"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_margin="16dp"
android:textSize="24sp"
android:text="CLICK ME"
android:id="#+id/click"/>
</RelativeLayout>
</android.support.v7.widget.CardView>
And here is the code that I am using now to solve the problem (this approach I do not like, I want to find better way)
public class PositionAwareCardView extends CardView {
private int mChildPosition;
public PositionAwareCardView(Context context) {
super(context);
}
public PositionAwareCardView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public PositionAwareCardView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setChildPosition(int pos) {
mChildPosition = pos;
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// if it's not the topmost view in view hierarchy then block touch events
return mChildPosition != 0 || super.onInterceptTouchEvent(ev);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
return false;
}
}
EDIT 2: I've forgotten to mention, this problem is present only on pre-Lolipop devices, it seems that starting from Lolipop, ViewGroups also take elevation into consideration while dispatching touch events
EDIT 3: Here is my current child drawing order:
#Override
protected int getChildDrawingOrder(int childCount, int i) {
return childCount - i - 1;
}
EDIT 4: Finally I was able to fix the problem thanks to user random, and this answer, the solution was extremely simple:
#Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (!onInterceptTouchEvent(ev)) {
if (getChildCount() > 0) {
final float offsetX = -getChildAt(0).getLeft();
final float offsetY = -getChildAt(0).getTop();
ev.offsetLocation(offsetX, offsetY);
if (getChildAt(0).dispatchTouchEvent(ev)) {
// if touch event is consumed by the child - then just record it's coordinates
x = ev.getRawX();
y = ev.getRawY();
return true;
}
ev.offsetLocation(-offsetX, -offsetY);
}
}
return super.dispatchTouchEvent(ev);
}
CardView uses elevation API on L and before L, it changes the shadow size. dispatchTouchEvent in L respects Z ordering when iterating over children which doesn't happen in pre L.
Looking at the source code:
Pre Lollipop
ViewGroup#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
final boolean customOrder = isChildrenDrawingOrderEnabled();
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder ?
getChildDrawingOrder(childrenCount, i) : i;
final View child = children[childIndex];
...
Lollipop
ViewGroup#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
// Check whether any clickable siblings cover the child
// view and if so keep track of the intersections. Also
// respect Z ordering when iterating over children.
ArrayList<View> orderedList = buildOrderedChildList();
final boolean useCustomOrder = orderedList == null
&& isChildrenDrawingOrderEnabled();
final int childCount = mChildrenCount;
for (int i = childCount - 1; i >= 0; i--) {
final int childIndex = useCustomOrder
? getChildDrawingOrder(childCount, i) : i;
final View sibling = (orderedList == null)
? mChildren[childIndex] : orderedList.get(childIndex);
// We care only about siblings over the child
if (sibling == child) {
break;
}
...
The child drawing order can be overridden with custom child drawing order in a ViewGroup, and with setZ(float) custom Z values set on Views.
You might want to check custom child drawing order in a ViewGroup but I think your current fix for the problem is good enough.
Did you try to set your topmost view as clickable ?
button.setClickable(true);
If this attribute is not set (as default) in the View XML it propagate the click event upwards.
But if you set it on the topmost view as true, it shouldn't propagate any event on any other view.
public class MainActivity extends Activity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TableView tv = new TableView(this);
tv.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT));
setContentView(tv);
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}
}
public class TableView extends ViewGroup {
private Paint oval;
private RectF rect;
public TableView(Context context) {
super(context);
oval= new Paint(Paint.ANTI_ALIAS_FLAG);
oval.setColor(Color.GREEN);
}
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawOval(rect , oval);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wspec = MeasureSpec.makeMeasureSpec(
getMeasuredWidth(), MeasureSpec.EXACTLY);
int hspec = MeasureSpec.makeMeasureSpec(
getMeasuredHeight(), MeasureSpec.EXACTLY);
for(int i=0; i<getChildCount(); i++){
View v = getChildAt(i);
v.measure(wspec, hspec);
}
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
float w=r-l;
float h=b-t;
rect=new RectF(w/8,h/8,7*w/8,7*h/8);
float theta = (float) (2 * Math.PI / getChildCount());
for(int i=0; i< getChildCount(); i++) {
View v = getChildAt(i);
w = (rect.right-rect.left)/2;
h = (rect.bottom-rect.top)/2;
float half = Math.max(w, h)/2;
float centerX = rect.centerX()+w*FloatMath.cos(theta);
float centerY = rect.centerY()+h*FloatMath.sin(theta);
v.layout((int)(centerX-half),(int)(centerY-half),(int)(centerX+half),(int)(centerY+half));
}
}
}
Well there are almost NONE good and deep tutorials and hardly any piece of data on how to do custom layouts right, so i tried to figure out how its done, what i am trying to implement is a Layout that paints a green oval at the center of the screen, and i want every child of this layout to be layed out around the oval.
you can think of that oval as a poker table that i want the children of this layout to seat around it.
What currently happens by this code is that i get a white app scren with no oval, so i debugged it and saw that onDraw never gets called...
3 questions:
why is onDraw not getting called?
the sdk warns me that i shouldnt allocate new objects within onLayout method, so where should i calculate the RectF so it is ready for the onDraw call to come?
does calling super.onDraw() would make all children paint themselves? or should i explicitly invoke their draw()?
If I got it all wrong and you guys can guide me in the right direction, or have any links to examples or tutorials related to this subject that would be helpful too!
By default, onDraw() isn't called for ViewGroup objects. Instead, you can override dispatchDraw().
Alternatively, you can enable ViewGroup drawing by calling setWillNotDraw(false) in your TableView constructor.
EDIT:
For #2:
- Initialize it in the constructor, then just call rect.set() in your onLayout() method.
For #3:
- Yes, as far as I'm aware the super call will handle it, you shouldn't have to handle that unless you need to customize the way the children are drawn.
If you want that the canvas is be re-draw call invalidate(), and the method onDraw() will be re-executed
I have a custom grid which extends ViewGroup and I have the next onLayout and measureChildrenSizes methods:
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// .. tileSide calcultion ..
measureChildrenSizes(tileSide);
for (int i = 0; i < getChildCount(); i++) {
Square child = (Square) getChildAt(i);
int x = child.getColumn();
int y = child.getRow();
child.layout(x * tileSide, y * tileSide,
(x + 1) * tileSide, (y + 1) * tileSide);
}
}
private void measureChildrenSizes(int tileSide) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
measureChild(child, tileSide, tileSide);
}
}
The childs (Square) are custom Views which have a custom onDraw method and the onTouch callback method:
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
rect.set(0, 0, getWidth(), getHeight());
gradient.setBounds(rect);
gradient.draw(canvas);
rectangle.setStrokeWidth(0.2f);
rectangle.setStyle(Style.STROKE);
rectangle.setColor(getResources().getColor(
R.color.puzzle_foreground));
canvas.drawRect(rect, rectangle);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
requestFocus();
changeState();
return false;
}
return super.onTouchEvent(event);
}
This draw a grid of Squares which side measure is tileSide. All the Squares works fine except the last column. Sometimes they don't respond.
Perhaps I'm looking for in the wrong direction.
Edit: in the emulator works fine. It fails in real Devices (tested on Sony xperia u, Samsung galaxy Ace, Samsung galaxy mini).
Returning false in your MotionEvent.ACTION_DOWNcan cause some of the GestureDetector(if that's what you are using) methods to not be called. Try returning true there and see if it responds better.
This Android Developers page might help you: Making a View Interactive
Sometimes bad touch responses can be caused by breaking cycle of managing views by Adapter in AdapterView. I have experienced the same issue when I saved
reference to views managed by AdapterView. It can cause bad response to touches.
how can I set scroll of scrollview to x pixels, before it's even shown?
In ScrollView I have some Views and i know that it will fill screen. Can I scroll to let say second View, so that first View is not visible when activity is started?
Now I have sth like this, but I think it's not perfect, sometimes, I see first View, and then it's scrolled to the second one
#Override
public void onGlobalLayout() {
if (mHorizontalScrollView.getChildCount() > 0 && mHorizontalScrollView.getChildAt(0).getWidth() > mScreenWidth) {
hsv.scrollTo(100, 0);
}
}
EDIT!!
Second attempt was to use http://developer.android.com/reference/android/view/ViewTreeObserver.OnPreDrawListener.html instead of http://developer.android.com/reference/android/view/ViewTreeObserver.OnGlobalLayoutListener.html
In OnPreDrawListener we can read that At this point, all views in the tree have been measured and given a frame. Clients can use this to adjust their scroll bounds or even to request a new layout before drawing occurs. so basically at this point we should adjust scrolling. So I created:
#Override
public boolean onPreDraw() {
if (hsv.getChildCount() > 0 && hsv.getChildAt(0).getWidth() > mScreenWidth) {
hsv.scrollTo(100, 0);
return false;
}
return true;
}
but now it's never working for me.
I combined onGlobalLayout with code below. It's not so great to use that hack but it still quite efficient. When I get onGlobalLayout event I check if layouts are done and then if it's ok I use method below.
// hsv - horizontal scroll view
private void forceShowColumn(final int column) {
int scrollTo = childViewWidth * column;
if (scrollTo == hsv.getScrollX()) {
return;
}
hsv.scrollTo(scrollTo, 0);
hsv.postDelayed(new Runnable() {
#Override
public void run() {
forceShowColumn(column);
}
}, 10);
}
try View.post(Runnable) in onCreate-method.
I solved this issue by implementing View.OnLayoutChangeListener in the fragment that controls my ScrollView.
public class YourFragment extends Fragment implements View.OnLayoutChangeListener{
...
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
view = inflater.inflate(R.layout.your_fragment_layout, container, false);
//set scrollview layout change listener to be handled in this fragment
sv = (ScrollView) view.findViewById(R.id.your_scroll_view);
sv.addOnLayoutChangeListener(this);
return view;
}
...
public void onLayoutChange (View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom){
switch (v.getId())
{
case R.id.your_scroll_view:
{
sv.scrollTo(0, sv.getTop());
}
}
}
The correct way to do this, is to call scrollTo() on the ScrollView after the view has been through its measurement and layout passes, otherwise the width of the content in the scrollView will be 0, and thus scrollTo() will not scroll to the right position. Usually after the constructor has been called and after the view has been added to its parent.
See http://developer.android.com/guide/topics/ui/how-android-draws.html for more insight on how android draws views.
MrJre gives the most elegant solution: "override onLayout() in the scrollview and do it there after super.onLayout()"
Example code here:
https://stackoverflow.com/a/10209457/1310343
Use :
scrollTo (int x, int y)
It sets the scrolled position of your view. This will cause a call to onScrollChanged
For more details : see this
ScrollView ScrollTo