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>
Related
I need to create a horizontal list of items that only displays fully visible items.
But as you can see, my recycler view show a particular element. I use a horizontal LinearLayoutManager.
I add 10 elements, but recycler view has room only for 3. I need to show only 3, but it always show me 3 and particular element.
<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"
tools:context=".MainActivity">
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recycler"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintEnd_toStartOf="#+id/textView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
My item layout:
<LinearLayout
android:id="#+id/itemLayout"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="10dp">
<TextView
android:id="#+id/tvAnimalName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ASDAS"
android:background="#color/colorAccent"
android:padding="10dp"
android:textSize="17sp"/>
</LinearLayout>
Adapter and activity are plain.
How can I show only visible 3 items?
Edit.
I must to disable scroll. So i am using:
layoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) {
#Override
public boolean canScrollHorizontally() {
return false;
}
};
Edit 2. These methods show -1 always:
int findLastVisibleItemPosition();
int findLastCompletelyVisibleItemPosition();
class HideLastDecorator() : RecyclerView.ItemDecoration() {
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
val count = parent.childCount
for (i in 0 until count) {
parent.getChildAt(i).visibility = if (count == i - 1) View.INVISIBLE else View.VISIBLE
}
}
}
and add it to your recyclerView Decorations
appsRecyclerView.addItemDecoration(HideLastDecorator())
Sorry for Kotlin :)
View.INVISIBLE is important, because if the View becomes GONE, it will be removed from the measuring of the RecyclerView's content and the new ViewHolder would be added.
I prefer to work careful with OnClickListener if any is set for the ViewHolder's content.
Below code will work for you. A little explanation: Extend RecyclerView and override onLayout() method. Once RecyclerView is ready iterate through all visible (on-screen) children of RecyclerView and apply your logic. In our case we'll draw BounddingBox for every nth child and RecyclerView. If child's bounds lie inside RecyclerView's bounds then show that child otherwise set visibility to GONE/INVISIBLE.
public class CustomRecyclerView extends RecyclerView {
Rect recyclerViewBounds = new Rect();
Rect currentChildViewBounds = new Rect();
public CustomRecyclerView(#NonNull Context context) {
super(context);
}
public CustomRecyclerView(#NonNull Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
}
public CustomRecyclerView(#NonNull Context context, #Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
recyclerViewBounds.set(l, t, r, b);
for (int i = 0; i < getChildCount(); i++) {
View currentChild = getChildAt(i);
currentChildViewBounds.set(currentChild.getLeft(), currentChild.getTop(), currentChild.getRight(), currentChild.getBottom());
currentChild.setVisibility(recyclerViewBounds.contains(currentChildViewBounds) ? VISIBLE : GONE); // or INVISBLE instead of GONE
}
}
}
And most importantly: In your xml file use com.your.packagename.CustomRecyclerView instead of androidx.recyclerview.widget.RecyclerView.
NOTE: Please refrain from any object initialization inside onLayout(). What I mean is don't move the object initializations inside onLayout() to make it "fancier".
None of the proposed answer worked as expected, so there ItemDecoration that i made, it checks if view completely visible in layout manager, and hide rest of views
class HideNotFullyVisibleDecorator : RecyclerView.ItemDecoration() {
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
val count = parent.childCount
for (i in 0 until count) {
val currentChild = parent.getChildAt(i)
currentChild.visibility =
if (parent.layoutManager?.isViewPartiallyVisible(currentChild, true, false) == true)
View.VISIBLE
else
View.GONE
}
}
}
Usage: recycler.addItemDecoration(HideNotFullyVisibleDecorator())
Also in my case I disabled scrolling for recycler
Recycler view is a Scrollable container which holds viewholders and recycles on scroll-up and down,
So it will display as much data as possible on screen, and that fourth half-visible item that you've shown in screenshot is just default behaviour of every scrollable view in android.
You have to customize your viewholders to adjust accordingly on runtime so that only fully visible items should be rendered.
You can do something like:
val availableWidth = screenWidth - (textViewWidth)
val itemWidth = (availableWidth / 3)
I have a RecyclerView which uses as its child layout a ViewGroup.
RecyclerView :
#Override
public TileDetailsHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.weather_tile_details_c, parent, Constant.SHOULD_ATTACH_NOW);
return new TileDetailsHolder(view);
}
#Override
public void onBindViewHolder(TileDetailsHolder holder, int position)
{
holder.setSavedPosition(tileList.get(position).getSavedPosition());
position);
holder.setDetailsRowList(tileList.get(position).getDetailRows());
}
XML :
<views.WeatherDetailLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/weatherDetailsLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<views.WeatherDetailHeaderView
android:id="#+id/weatherDetailsHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<views.WeatherDetailDaysView
android:id="#+id/weatherDetailDays"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<android.support.v7.widget.RecyclerView
android:id="#+id/detailInnerRV"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#android:color/holo_green_light"/>
</views.WeatherDetailLayout>
ViewGroup :
public class WeatherDetailLayout extends ViewGroup
{
private int width;
private int height;
private int leftPos = 0;
private int rightPos = 0;
private int topPos = 0;
private int[] bottomPos = new int[3]; //3 = number of views
public WeatherDetailLayout(Context context) { super(context); init(); }
public WeatherDetailLayout(Context context, #Nullable AttributeSet attrs)
{ super(context, attrs); init(); }
public void init()
{
width = (int) Util.getScreenWidthInPixels();
height = (int) Util.getScreenHeightInPixelsNoToolbarOrStatusbar();
rightPos = width;
bottomPos[0] = (int) (height * 0.40);
bottomPos[1] = (int) (height * 0.20);
bottomPos[2] = (int) (height * 0.40);
}
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom)
{
for(int i = 0; i < bottomPos.length; i++)
{
getChildAt(i).layout(leftPos, topPos, rightPos, topPos + bottomPos[i]);
topPos += bottomPos[i];
}
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
}
private int measureWidth(int measureSpec)
{
return resolveSizeAndState(width, measureSpec, 0);
}
private int measureHeight(int measureSpec)
{
return resolveSizeAndState(height, measureSpec, 0);
}
}
This code gives 3 views which are inside of the ViewGroup that have the same width as the screen and basically
40 percent of the total height for the first, 20 for the second and 40 for the third.
The RecyclerView is setup with a horizontal layout manager. Only one item is visible at a time. The user can then scroll to the left or the right a certain number of items let say 10. As the user scrolls the left most or right most item is removed and a new one is added depending on which way the user scrolled/swiped.
1 2 3 4 5 6 7 8 9 10
User starts at 5, scrolls to 6, 4 gets removed, 7 gets added.
The first N (usually 3 in this example) items get drawn fine. Meaning when the RecyclerView is using onCreateViewHolder. However, when the RecyclerView starts recycling views and using onBindViewHolder nothing gets shown anymore. Using log statement I can see it goes into the ViewGroup and goes onLayout and onMeasure. Everything get calculated the childGetCount method returns 3 views but nothing gets shown. Simply get the background of the activity like so.
In the above pic the layout bounds don't even get shown only the outer layout of the recyclerView gets shown. (Using the option on the phone Show layout bounds).
When I do this but instead of the custom viewgroup I use a LinearLayout but the same custom views this does not happen. It shows the views every time. I can also see that it goes into onCreateViewHolder only 3 times as well.
I am looking for any leads or answers to resolve this issue.
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()
}
}
I am trying to create a custom ViewGroup, and I want to use it with a full screen application. I am using the "requestWindowFeature(Window.FEATURE_NO_TITLE)" to hide the title bar. The title bar is not showing, but it still consuming space on top of the window.
The image above was generated with the following code:
public class CustomLayoutTestActivity extends Activity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
Button b = new Button(this);
b.setText("Hello");
CustomLayout layout = new CustomLayout(this);
layout.addView(b);
setContentView(layout);
}
}
public class CustomLayout extends ViewGroup {
public CustomLayout(Context context) {
super(context);
}
public CustomLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
Log.i("CustomLayout", "changed="+changed+" l="+l+" t="+t+" r="+r+" b="+b);
final int childCount = getChildCount();
for (int i = 0; i < childCount; ++i) {
final View v = getChildAt(i);
v.layout(l, t, r, b);
}
}
}
(The full Eclipse project is here)
It is interesting to see that it is the Android that is given this space for my custom layout. I am setting the CustomLayout as the root layout of my Activity. In the Log in the "onLayout" is receiving "t=25", and that is what is pushing my layout down. What I don't know is what I am doing wrong that makes Android the "t=25" (which is exactly the height of the title bar).
I am running this code in the Android SDK 2.1, but I also happens in Android 2.2.
EDIT: If I change the CustomLayout class for some default layout (such as LinearLayout), the space disappears. Of course, the default layouts of Android SDK don't create the layout I am trying to create, so that is why I am creating one.
Although the layout I am creating is somewhat complex, this is the smallest code I could create reproducing the problem I have with my layout.
It's not a full answer, but in the meantime you can work around the problem by wrapping your custom layout in a <FrameLayout />
Also, it's worth noting that your layout extends beyond the bottom of the screen. It's shifted down by the title bar height (38 pixels in my emulator)
Edit: Got it. onLayout() (and the corresponding layout() method) specify that the coordinate are not relative to the screen origin, they're relative to the parent ( http://developer.android.com/reference/android/view/View.html#layout%28int,%20int,%20int,%20int%29 ). So the system is telling you that you're at relative coordinates (0, 38), and you're adding it when passing that down to your child, which means that you're saying that your child is at screen coordinates (0, 76), causing the gap.
What you actually want to do is:
v.layout(0, 0, r - l, b - t);
That will put your child Views aligned with the top left corner of your View, with the same width and height as your view.
I had the same issue with a FrameLayout in 2.2
I fixed it by adding android:layout_gravity="top" to the FrameLayout
I am doing an app with gallery with showing a few images, when I scroll the images, they move and jump after a certain point. How do I make them smooth? Any sample code would be of great help.
I had similar problem. Looks like it can be caused by changes in layout, e.g. if you change text in textview which has wrap_content width. This cases layout change and probably forces gallery to update itself and it snaps right on current item.
I was able to fix it by playing with layout, setting fixed sizes where I could etc. but I don't know about permanent and reliable solution
EDIT: also I found this hack if above doesn't work for you
http://www.unwesen.de/2011/04/17/android-jittery-scrolling-gallery/
I managed to solve this problem by overriding the onLayout() method in the Gallery parent and then ignoring any calls where the changed flag was not true.
public class MyGallery extends Gallery {
public MyGallery(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
super.onLayout(changed, l, t, r, b);
}
}
}
I found the above Gallery extends solution to work fairly well. However it was still causing some jitter. By simply overriding the onLayout method and look for number of views on screen I ended up with a "smooth as silk" Gallery view.
Note that I use this for a full screen slideshow effect.
public class SmoothGallery extends Gallery {
public SmoothGallery(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int viewsOnScreen = getLastVisiblePosition() - getFirstVisiblePosition();
if(viewsOnScreen <= 0)
super.onLayout(changed, l, t, r, b);
}
}