Subclassed RelativeLayout onDraw() method not called in ListView - android

Here's a little background into what I'm trying to achieve, in order to help make my question a bit more clear...I'm creating a Navigation Drawer where each item in the ListView looks similar to the following:
However, I need to be able to change the color of the right side border (the blue) to a variety of different colors programmatically, so while playing around with a solution, I decided to extend a RelativeLayout and draw the line in the onDraw(Canvas c); method. My RelativeLayout code is as follows:
public class CustomRelativeLayout extends RelativeLayout {
private final Paint paint = new Paint();
public CustomRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
//Other Constructors
private void init() {
setPaintColor(Color.argb(128, 0, 0, 0));
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(3);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawLine(getMeasuredWidth() - 1, 0, getMeasuredWidth() - 1, getMeasuredHeight(), paint);
}
public void setPaintColor(int color){
paint.setColor(color);
invalidate();
}
}
My NavigationDrawer's ListView also contains a header that uses this class, and it works fine as a header view. However, for each individual ListView item, the border isn't present. I've debugged my solution, and found that my subclassed RelativeLayout's onDraw(Canvas c); method is called for the header view, but isn't called for each of the ListView's child views provided by my ArrayAdapter<String>.
I know there are other ways to handle this, such as using a default View, setting it's background to the color I want, and aligning it to the right - but that's not my question. My question is why is my CustomRelativeLayout's onDraw(Canvas c); method is called for the ListView's header view, and not for each of the Views provided by my adapter? Any insight into this behavior would be appreciated. Also, here are the CustomArrayAdapter and nav_drawer_item.xml used with the ListView in case they're helpful:
public class SimpleDrawerAdapter extends ArrayAdapter<String> {
public SimpleDrawerAdapter(Context context, int resource,
String[] sections) {
super(context, resource, sections);
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
RelativeLayout container = null;
if(convertView == null){
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
container = (CustomRelativeLayout) inflater.inflate(R.layout.nav_drawer_item, parent, false);
} else {
container = (CustomRelativeLayout) convertView;
}
((TextView)container.findViewById(R.id.nav_item_text)).setText(getItem(position));
return container;
}
}
nav_drawer_item.xml
<?xml version="1.0" encoding="utf-8"?>
<com.mypackage.views.CustomRelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="#dimen/navigation_drawer_width"
android:layout_height="wrap_content">
<TextView
android:id="#+id/nav_item_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#color/nav_drawer_grey"
android:textSize="#dimen/text_large"
android:layout_margin="#dimen/navigation_drawer_item_margin"
android:paddingTop="4dp"
android:paddingBottom="4dp" />
</com.mypackage.views.CustomRelativeLayout>

Have you tried clearing the WILL_NOT_DRAW flag by calling setWillNotDraw method in your custom layout?
If this view doesn't do any drawing on its own, set this flag to allow
further optimizations. By default, this flag is not set on View, but
could be set on some View subclasses such as ViewGroup. Typically, if
you override onDraw(android.graphics.Canvas) you should clear this
flag.

just call it yourself each time you iterate over the list
if(convertView == null){
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
container = (CustomRelativeLayout) inflater.inflate(R.layout.nav_drawer_item,
parent, false);
if(container!=null){
container.draw()
}
} else {
container = (CustomRelativeLayout) convertView;
if(container!=null){
container.draw()
}
}

Related

Views in ViewGroups in a ViewGroup

I've implemented my own custom ViewGroup, containing zero or more other ViewGroup. All ViewGroups and Views are inflated from XML resources. Leaving out some trivial parts, the code looks roughly like this:
public class OuterLayout extends ViewGroup {
public OuterLayout(Context context, AttributeSet attributes) {
super(context, attributes);
setWillNotDraw(false);
}
#Override
public void onLayout(boolean changed, int l, int t, int r, int b) {
int c = getChildCount(); View v;
for(int i = 0; i < c; i++) {
v = getChildAt(i);
// in reality different values are calculated for each child
v.layout(l, t, r, b);
}
}
}
public class InnerLayout extends ViewGroup {
public InnerLayout (Context context, AttributeSet attributes) {
super(context, attributes);
setWillNotDraw(false);
}
#Override
public void onLayout(boolean changed, int l, int t, int r, int b) {
View v = findViewById(R.id.childView);
// in reality different values are calculated for each child
v.layout(l, t, r, b);
}
}
public class ChildView extends View {
#Override
public void onDraw(Canvas canvas) {
// do some drawing
}
}
The ultimate problem here is that the ChildViews aren't being drawn at all, but the InnerLayouts are. The onDraw method in ChildView isn't called at all, but the onLayoutmethods in both of the ViewGroups are. I've more or less solved this problem by implementing the onDraw method in the InnerLayout and calling the draw method of all ChildViews, but I feel like this is not the best solution for this problem and might eventually lead to other problems, especially because the Canvas of the InnerLayout is passed to the ChildView.
In an earlier stage of development the ChildViews where locatied inside the OuterLayout, which worked fine. My question is, why doesn't it work when the InnerLayout is placed in between?
Edit: the XML from which the ViewGroups and Views are inflated. The InnerLayout is added to the OuterLayout programmatically.
<?xml version="1.0" encoding="utf-8"?>
<com.example.OuterLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<?xml version="1.0" encoding="utf-8"?>
<com.example.InnerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.ChildView
android:id="#+id/childView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.example.OuterLayout>

Using CustomBottomSheetBehavior in an Scrolling Activity causes an empty space behind tool-bar

I am recently using CustomBottomSheetBehavior to make an googlemaps like bottom sheet behavior and it works great. I have only one problem.please look at this image
If I use it in a scrolling activity the content of tool-bar covers my list box. so I ahve to add margin-top to my list view. It works but when I draw bottomsheet up toolbar goes up and behind it, there is an empty space. This is because I have added some margin top to make my list's top visible. Is there any way to connect list's margin top to the amount of moving bottom-sheet and when It moves up decrease margin value to and when it moves down increase it?or is there any better way ?
It seems I have to develope my own TopMarginBehavior for this job but I have no idea how to do it.
thanks
Create your own class related to the behavior you want (MarginTopBehavior)
Extends it from CoordinatorLayout.Behavior
Now you have to focus on 2 methods: layoutDependsOn and onDependentViewChanged. With the first one you are selecting the view that your MarginTopBehavior is following, in this case is a NestedScrollView. With the second one you are reacting (the magic!) when the scroll get moved.
At this point you get this:
public class MarginTopBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
private FrameLayout.LayoutParams mLayoutParams;
public MarginTopBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof NestedScrollView;
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
}
}
The logic to be applied in onDependentViewChanged is just this:
* Define the cap (min/max margin value) and controls when the margin value has reached one of those cap.
* Update margin value while the values are between the caps. In this point you have to implement an algorithm about what you want (parallax, linear, etc). That is what I'm calling THE_MAGIC_ECC in the next code:
public class MarginTopBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
/**
* Params of the component you want to modify the margin
*/
private FrameLayout.LayoutParams mLayoutParams;
/**
* Used to access DIMENS in your project
*/
private Context mContext;
private int mMinYvalue;
private int mMaxYValue;
public MarginTopBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
}
#Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof NestedScrollView;
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
if (mLayoutParams == null) {
mLayoutParams = (FrameLayout.LayoutParams) child.getLayoutParams();
}
if (dependency.getY() <= mMinYvalue) {
mLayoutParams.setMargins(0, 0, 0, 0);
child.setLayoutParams(mLayoutParams);
return true;
}
else if (dependency.getY() > mMinYvalue && dependency.getY() <= mMaxYValue) {
int THE_MAGIC_ECC = 1 + 2 + 3;
mLayoutParams.setMargins(0, 0, 0, THE_MAGIC_ECC );
child.setLayoutParams(mLayoutParams);
return true;
}
else {
mLayoutParams.setMargins(0, 0, 0, 100);
child.setLayoutParams(mLayoutParams);
return true;
}
}
}

Add a ProgressBar into a custom view

I could not find tips or examples about how to do this. I want to add a progressbar over a Rect that I have already drawn, so how I can I do this?
Your answers will be truly appreciated! :)
Edited
public class MainView extends View {
public MainView(Context context) {
super(context);
}
public MainView(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
logo.set(getPX(5), getPX(10), getPX(65), getPX(70));
background.set(getPX(30), getPX(2), canvas.getWidth() - getPX(10),
getPX(81));
canvas.drawRect(background, paint);
canvas.drawRect(logo, paint);
}
final private int getPX(float dp) {
return (int) (getResources().getDisplayMetrics().density * dp);
}
}
Based on your implementation (as you shown it to me):
<ScrollView .. >
<LinearLayout .. >
<MainView .. />
<MainView .. />
<MainView .. />
</LinearLayout>
</ScrollView>
my proposed solution would be - extend your CustomView class with LinearLayout (because then you can add additional views to your custom view):
public class MainView extends LinearLayout {
private Rect logo;
private Rect background;
public MainView(Context context) {
super(context);
setWillNotDraw(false); //needed in order to call onDraw method
logo = new Rect();
background = new Rect();
}
public MainView(Context context, AttributeSet attrs) {
super(context, attrs);
setWillNotDraw(false);
logo = new Rect();
background = new Rect();
RelativeLayout progressBarLayout = new RelativeLayout(context);
RelativeLayout.LayoutParams lay = new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.FILL_PARENT,
RelativeLayout.LayoutParams.WRAP_CONTENT);
ProgressBar progressBar = new ProgressBar(context, null, R.attr.progressBarStyleHorizontal);
progressBar.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.FILL_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT));
progressBar.setIndeterminate(true); //remove that (only for demonstration purposes)
lay.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
progressBarLayout.addView(progressBar, lay);
addView(progressBarLayout);
//Apart from the ProgressBar you are able to add as many views as you want to your custom view and align them as you would like
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
logo.set(getPX(5), getPX(10), getPX(65), getPX(70));
background.set(getPX(30), getPX(2), canvas.getWidth() - getPX(10),
getPX(81));
canvas.drawRect(background, paint);
canvas.drawRect(logo, paint);
}
final private int getPX(float dp) {
return (int) (getResources().getDisplayMetrics().density * dp);
}
}
In this example I am only showing how to add ProgressBar component, because that was your original question
You will need to add some more code in order to carry out your requirements (got from questioner):
P.S. This is just a solution to your particular problem/request. I would advise to use ListView component, which is better in the sense that it reuses views, so in such cases where you will have LOTS OF custom view instances, your application might become unusable, because there will be too much load on the activity class.
In order for you to migrate to using ListView component, try some examples first, like this

The imageView onClick event of first item in GridView can't work or delay

I've designed a GridView with custom item layout and there is one ImageView on the top-left corner, another ImageView on the top-right corner, a main ImageView and Text in the center.
I need to set the OnClickListener for both two ImageView on the corner and the OnItemClickListener for the adapter.
Everything worked fine but only the first item in the GridView can't trigger the OnClickListener. Can anyone solve this problem?
P.S. I found the first item ImageView's OnClickListener will trigger when I do the next action(e.g. Click other items)
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
TagView tag;
if (convertView == null) {
v = m_LayoutInflater.inflate(R.layout.mydoc_grid_item, null);
tag = new TagView(
(ImageView) v.findViewById(R.id.myDoc_GridItem_IV),
(TextView) v.findViewById(R.id.myDoc_GridItem_TV),
(ImageView) v.findViewById(R.id.iViewAdd2Category),
(ImageView) v.findViewById(R.id.iViewDelete));
v.setTag(tag);
} else {
tag = (TagView) v.getTag();
}
v.setLayoutParams(new GridView.LayoutParams((int) (180 * v
.getResources().getDisplayMetrics().density), (int) (180 * v
.getResources().getDisplayMetrics().density)));
tag.image.setScaleType(ImageView.ScaleType.CENTER_CROP);
tag.image.setImageDrawable(new BitmapDrawable(BitmapFactory.decodeFile(m_DisplayItems.get(position)
.get("image").toString())));
tag.text.setText(m_DisplayItems.get(position).get("text").toString());
tag.iAdd2Category.setVisibility((m_IsEdit) ? View.VISIBLE : View.GONE);
tag.iDelete.setVisibility((m_IsEdit) ? View.VISIBLE : View.GONE);
if (m_IsEdit) {
tag.iAdd2Category.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
Log.i(LOG_TAG, "Click on image add 2 category");
}
});
tag.iDelete.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
Log.i(LOG_TAG, "Click on image delete");
}
});
}
return v;
}
Mixing OnClickListeners and OnItemClickListeners for list's items doesn't work well.
In your case you should probably use multiple OnClickListeners on the different parts of your item and not use OnItemClickListener at all.
I finally found the answer in this article.
OnClickListener not working for first item in GridView
In my case:
if (convertView == null) {
v = m_LayoutInflater.inflate(R.layout.mydoc_grid_item, null);
tag = new TagView(
(ImageView) v.findViewById(R.id.myDoc_GridItem_IV),
(TextView) v.findViewById(R.id.myDoc_GridItem_TV),
(ImageView) v.findViewById(R.id.iViewAdd2Category),
(ImageView) v.findViewById(R.id.iViewDelete));
v.setTag(tag);
} else {
tag = (TagView) v.getTag();
}
just remove if else statement and the "tag" will be instantiated every time.
The reason for this problem is, that Android calls getView() for the first item not only when the View is drawn, but also for Layout-Measuring! There is no way to differ within getView() between a measure call and a draw call. So when you first have a draw call, and at some later point a measure call you overwrite the onClickListener.
Actually it seems right to do so - since you do it every time, but i observed this behaviour. Maybe you set the clickListener and the system releases it after the getView call because the view won't be drawn... I didn't track that deep enough to assure.
Nevertheless it is a bad idea to recreate the view every time since it is VERY time expensive - your grid might not scroll smooth.
The solution is to write your own GridView as follows:
public class MyGridView extends GridView{
private GridAdapter adapter;
public MyGridView(Context context) {
super(context);
}
public MyGridView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public MyGridView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setAdapter(GridAdapter adapter) {
this.adapter = adapter;
super.setAdapter(adapter);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if(adapter!=null)
adapter.setIsMeasureCall();
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}}
and implement the method setIsMeasureCall() in your GridAdapter. The method only sets a flag to true whenever it is called.
Then in your getView() method you check if it is a measure call and if this is true - you do not set the Listeners.
Make sure to set isMeasure to false at the end of your getView() method.
I hope everything is clear :)
For more information you can check out my homepage where I provide a full Android project that solves this problem.
http://simon.osim.at/projects/gridadapter/#TheSolution
the selector file
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_pressed="true" android:drawable="#drawable/gridview_item_shape"></item>
<item android:state_pressed="false" android:drawable="#android:color/transparent"></item>
</selector>
the item layout file
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<ImageView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/griditem"
android:padding="2dp"
android:src="#drawable/grid_photo"
android:layout_centerInParent="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#drawable/gridview_item_select"
/>
</RelativeLayout>

Android: How to draw another View in a custom-View's onDraw() method

I need to create a custom view which is able to swipe between different views. I can't use ViewPager or the like, since it hasn't the transitions and features I need.
I started with this:
public final class DynamicSwipeView extends View {
private View view;
public DynamicSwipeView(Context context) {
super(context);
}
public void setPage(View view) {
this.view = view;
}
#Override
protected void onDraw(final Canvas canvas) {
if (view != null) {
view.draw(canvas);
}
}
}
But the other view (which works if used directly) isn't drawn. When I assign this swipe-view a background-color in the constructor, it's displayed in that color instead of white, but the other view isn't drawn nevertheless.
Here's a version I'm using written in Kotlin and it works nicely, tested on API 19, 25, and 28.
I'm using it for a floating header row for a TableLayout that I want anchored to the top of the screen. I assign viewToDraw in an ViewTreeObserver.onGlobalLayoutListener so I know the header row is laid out. This was convenient for me as I was listening to the view tree anyway, but this could be brought into the CopyView to invalidate() when necessary. The items in the header row and the CopyView are set to a fixed height, but you could expand on this to copy view dimensions too.
class CopyView #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {
var viewToDraw: View? = null
set(value) {
field = value
background = value?.background
invalidate()
}
override fun onDraw(canvas: Canvas?) {
viewToDraw?.draw(canvas)
}
}
try this:
public final class DynamicSwipeView extends View {
private View view;
public DynamicSwipeView(Context context) {
super(context);
}
public void setPage(View view) {
this.view = view;
this.invalidate();
}
#Override
protected void onDraw(final Canvas canvas) {
super.draw(canvas);
if (view != null) {
view.draw(canvas);
}
}
}
this is just a suggestion, I haven't tried it.
If it doesn't work, you should use a list of view and manage visibility of these views (current view = VISIBLE, others GONE). It could be easier to do like that.

Categories

Resources