GridView with infinite scroll optimization - java

I've created a simple app that fetches images from Pixabay and then displays them in a GridView with infinite scroll.
My OnScrollListener:
public class BasicOnScrollListener implements AbsListView.OnScrollListener {
private IOnScroll onScroll;
public BasicOnScrollListener(IOnScroll action) {
this.onScroll = action;
}
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (firstVisibleItem + visibleItemCount >= totalItemCount - visibleItemCount)
onScroll.onReachedEnd();
}
}
Code resposible for data handling:
private List<Image> images = new ArrayList<>();
....
private void init() {
this.imageAdapter = new ImageAdapter(this, images);
this.gridView.setAdapter(imageAdapter);
populateGridView();
this.gridView.setOnScrollListener(new BasicOnScrollListener(() -> {
populateGridView();
}));
}
private void populateGridView() {
if (!isLoading) {
isLoading = true;
this.progressBar.setVisibility(View.VISIBLE);
imagesRepository.getImages(currentPage, PAGE_SIZE, new IOnRepositoryDataReturn<ImagesList>() {
#Override
public void onData(ImagesList data) {
clearIfNeeded();
images.addAll(data.images);
imageAdapter.notifyDataSetChanged();
onFinish();
}
#Override
public void onError(String error) {
Toast.makeText(getApplicationContext(), error, Toast.LENGTH_LONG).show();
}
private void clearIfNeeded() {
if (images.size() > 1000) {
images.subList(0, 300).clear();
}
}
private void onFinish() {
progressBar.setVisibility(View.INVISIBLE);
isLoading = false;
currentPage = currentPage + 1;
}
});
}
}
Everything works fine but I'd like to optimize that. When there are already more than 1000 items in the GridView I'd like to remove the first 300 items, so I won't run into out of memory problems.
The problem is, when I simply remove the first 300 items from list (as shown in clearIfNeeded() method in IOnRepositoryDataReturn implementation), the screen shifts. I no longer see items that I've seen before removal.
Example image. Situation if first row (items 1-2) from images list is removed.
left image - grid before removing
center - grid after removing (as it is for now, removing items on top shifts all items up)
right - how i'd like it to behave (removing items somewhere up doesnt affect what is seen)
The black square is representing the GridView.
I'd like to somehow adjust the viewing position in GridView, so I'd still see the same images as before removal.
Layout xml:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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">
<ProgressBar
android:id="#+id/ProgressSpinner"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="#+id/GridView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="#+id/GridView" />
<GridView
android:id="#+id/GridView"
android:layout_width="match_parent"
android:layout_height="406dp"
android:layout_marginBottom="8dp"
android:numColumns="3"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</GridView>
Is it even possible with Grid View or should i look for some other possibility?

When there are already more than 1000 items in the GridView I'd like to remove the first 300 items, so I won't run into out of memory problems.
Is it even possible with Grid View or should I look for some other possibility?
Here is my 2 cents. If I understand correctly, your concern seems to be preventing OutOfMemory exceptions and improving memory usage and performance. If you implement the grid of photos using RecyclerView and GridLayoutManager, it would reduce many of your problems with memory. Your approach of trying to remove 300 items which are off screen, seems pretty cumbersome and error prone. Here is an excerpt from the official doc titled Create a List with RecyclerView:
RecyclerView:
The RecyclerView uses a layout manager to position the individual items on the screen and determine when to reuse item views that are no longer visible to the user. To reuse (or recycle) a view, a layout manager may ask the adapter to replace the contents of the view with a different element from the dataset. Recycling views in this manner improves performance by avoiding the creation of unnecessary views or performing expensive findViewById() lookups.
GridLayoutManager:
GridLayoutManager arranges the items in a two-dimensional grid, like the squares on a checkerboard. Using a RecyclerView with GridLayoutManager provides functionality like the older GridView layout.
You can find many tutorials for this on quick search eg: Android GridLayoutManager with RecyclerView

You can optimize this by:
Using RecycleView + Grid Layout instead of GridView. RecycleView will help your view scroll smoothly.
To reach scroll to bottom, I no longer use ScrollListener for better performance. I will provide you another option:
Create callback
public interface AdapterCallback {
void onReachLastItem();
}
Call onBindViewHolder:
#Override
protected void onBindContentViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
if (0 != position && position == getItemCount() - 1 && null != mCallback) {
mCallback.onReachLastItem();
}
}
Make your fragment or activity implement AdapterCallback to load more data.

Related

How to avoid "jumpy" issue when interacting with soft keyboard visibility

Currently, we have an app with the following requirements
Must use android:windowSoftInputMode="adjustPan"
Use ViewCompat.setWindowInsetsAnimationCallback and ViewCompat.setOnApplyWindowInsetsListener to interact with soft keyboard visibility with smooth animation.
Here is our code, when interacting with soft keyboard visibility. It works pretty well in the case, when our EditText is not scrollable.
The animation went pretty well, when keyboard is showing and hiding.
MainActivity.java
public class MainActivity extends AppCompatActivity {
EditText editText;
LinearLayout toolbar;
FrameLayout keyboardView;
private int systemBarsHeight = 0;
private int keyboardHeightWhenVisible = 0;
private boolean keyboardVisible = false;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
editText = findViewById(R.id.edit_text);
toolbar = findViewById(R.id.toolbar);
keyboardView = findViewById(R.id.keyboard_view);
final View rootView = getWindow().getDecorView().getRootView();
ViewCompat.setOnApplyWindowInsetsListener(rootView, (v, insets) -> {
boolean imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime());
systemBarsHeight = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom;
keyboardVisible = imeVisible;
if (keyboardVisible) {
keyboardHeightWhenVisible = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom;
}
// https://stackoverflow.com/questions/75325095/how-to-use-windowinsetscompat-correctly-to-listen-to-keyboard-height-change-in-a
return ViewCompat.onApplyWindowInsets(v, insets);
});
WindowInsetsAnimationCompat.Callback callback = new WindowInsetsAnimationCompat.Callback(
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP
) {
#NonNull
#Override
public WindowInsetsCompat onProgress(#NonNull WindowInsetsCompat insets, #NonNull List<WindowInsetsAnimationCompat> runningAnimations) {
// Find an IME animation.
WindowInsetsAnimationCompat imeAnimation = null;
for (WindowInsetsAnimationCompat animation : runningAnimations) {
if ((animation.getTypeMask() & WindowInsetsCompat.Type.ime()) != 0) {
imeAnimation = animation;
break;
}
}
if (imeAnimation != null) {
int keyboardViewHeight;
if (keyboardVisible) {
keyboardViewHeight = (int) (keyboardHeightWhenVisible * imeAnimation.getInterpolatedFraction()) - systemBarsHeight;
} else {
keyboardViewHeight = (int) (keyboardHeightWhenVisible * (1.0-imeAnimation.getInterpolatedFraction())) - systemBarsHeight;
}
keyboardViewHeight = Math.max(0, keyboardViewHeight);
ViewGroup.LayoutParams params = keyboardView.getLayoutParams();
params.height = keyboardViewHeight;
keyboardView.setLayoutParams(params);
Log.i("CHEOK", "keyboardVisible = " + keyboardVisible + ", keyboardViewHeight = " + keyboardViewHeight);
}
return insets;
}
};
ViewCompat.setWindowInsetsAnimationCallback(rootView, callback);
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<EditText
android:id="#+id/edit_text"
android:padding="16dp"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="top" />
<LinearLayout
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal"
android:background="#ffff00" />
<FrameLayout
android:id="#+id/keyboard_view"
android:background="#ff0000"
android:layout_width="match_parent"
android:layout_height="0dp" />
</LinearLayout>
Here is the outcome.
When EditText is not scrollable
However, our app becomes "jumpy", when the content of EditText is scrollable.
When EditText is scrollable, our app becomes "jumpy"
Does anyone know what is the root cause of this problem, and how we can resolve such?
A demo to demonstrate such an issue, can be downloaded from https://github.com/yccheok/programming-issue/tree/main/jumpy
Using both adjustPan and WindowInsetsAnimation seems to over-animating the content.
adjustPan - The activity's main window is not resized to make room for the soft keyboard. Rather, the contents of the window are automatically panned so that the current focus is never obscured by the keyboard and users can always see what they are typing. This is generally less desirable than resizing, because the user may need to close the soft keyboard to get at and interact with obscured parts of the window.
This means that the activity is pushing its upper part until it's possible to make a room for the EditText widget (or its editable part) visible so that the user can see what they are typing.
And I do believe that Google developers provided the insets API to get the obsolete overwhelming stuff deprecated soon or later; for instance recently setting adjustResize programmatically is now deprecated as of API Level 30 and replaced with the inset API.
More verification of the inconvenience between adjustPan and WindowInsetsAnimation, try the below couple of scenarios in your sample repo:
Remove android:windowSoftInputMode="adjustPan"
Remove ViewCompat.setWindowInsetsAnimationCallback(rootView, callback);
Either scenario will work solely without having to worry about the jumpy/bouncy layout. But in case of the scenario no. 1, the red view appears because the activity area doesn't occupy the entire window screen (this requires to have a full screen app) with WindowCompat.setDecorFitsSystemWindows(getWindow(), false);. This tutorial deeply targets different aspects of insets API.
What I think the cause of this jumpy/bouncy behavior is that the adjustPan tries to do its job when the keyboard is shown; but eventually hard-coding the margins in the WindowInsetsAnimation wins the round and makes the layout bounce back at the end of the animation.
So, it's recommended to remove that adjustPan to keep the new fancy inset APIs up and running.
Or at least keep it at the manifest file, but disable it just before starting the animation and re-enable it again at the end of the animation (i.e., disable its panning behavior during the animation) using onPrepare() and onEnd() callbacks:
WindowInsetsAnimationCompat
.Callback callback = new WindowInsetsAnimationCompat.Callback(
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP
) {
#Override
public void onPrepare(#NonNull WindowInsetsAnimationCompat animation) {
super.onPrepare(animation);
// disable adjustPan
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING);
}
#Override
public void onEnd(#NonNull WindowInsetsAnimationCompat animation) {
super.onEnd(animation);
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
}
#NonNull
#Override
public WindowInsetsCompat onProgress(#NonNull WindowInsetsCompat insets, #NonNull List<WindowInsetsAnimationCompat> runningAnimations) {
// ... code is omitted for simplicity
}
};
But make sure that you also have a full screen app:
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);

How to listen for multiple GlobalLayout events

I am trying to listen for GlobalLayout using
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
int c=0;
#Override
public void onGlobalLayout() {
c++; //without removing the listener c will grow for ever even though there is no GlobalLayout events
view.setText(""+c);
}
});
but it's called endlessly.
I know I should remove the listener like this: view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
But I want to keep the listener alive for the next GlobalLayout events.
Currently I am just trying to listen for view position changing using this question
I tried onPreDraw but it is the same.
Is it possible to listen for several GlobalLayout events?
Thanks in advance.
The code you posted as an example shouldn't compile because of issues with accessing the variable c from the listener. If you try it, you should get the following error in Android Studio:
Variable 'c' is accessed from within inner class, needs to be final or effectively final
We can take the suggestion that error check suggests and create a one-element array as follows:
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final int[] c = {0};
final View[] view = {findViewById(R.id.textView)};
view[0].getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
c[0]++;
}
});
}
}
If you run this code against the following layout, you will see that the listener is called twice which I think corresponds to two layout passes:
<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:id="#+id/layout"
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_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="#id/textView" />
</androidx.constraintlayout.widget.ConstraintLayout>
If the layout is somehow modified within the global layout listener, then it will trigger another layout and another call to the the listener. If the listener again makes a change, the listener will be called yet again and so on forever.
It would be helpful if you would post the actual code you are having trouble with or a simple project that demonstrates the problem. Is the layout somehow modified within the listener?
Update: As you say in one of your comments on this answer, your issue was that you made a change to a view in the global layout listener which triggered another layout and another call to the listener ad infinitum. Once you removed the code that made the layout change, that particular issue was resolved.
If you're just trying to trigger the code once the view position has changed, then you could probably just check if the x, y coordinates has changed before increasing the variable c
Something like this:
int c = 0;
int x = 0;
int y = 0;
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
int x2 = view.getX();
int y2 = view.getY();
// do not increase c unless position has changed
if (x2 != x && y2 != y) {
c++;
// update coordinates values
x = x2;
y = y2;
}
}
});
you can set the Listener on your Viewes try to use view.addOnLayoutChangeListener
https://developer.android.com/reference/android/view/View.OnLayoutChangeListener
it will give you the old and new X&Y of your layout and you can adjust your view base on it
Simply add a flag to detect if it is a normal event or a setText() that is called by the listener and skip it if needed.
Code not tested. But I guess you will get the idea.
int c=0;
view.getViewTreeObserver().addOnGlobalLayoutListener(new CustomLayoutListener() {
int c=0;
#Override
public void onGlobalLayout() {
if(skipEvent)
return;
c++;
//flag skipEvent while performing setText()
skipEvent = true;
view.setText(""+c);
skipEvent = false;
}
});
class CustomLayoutListener extends ViewTreeObserver.OnGlobalLayoutListener {
static boolean skipEvent;
#Override
public void onGlobalLayout() {}
}

Animate view width with Google Databinding

I would like to animate a view width when the value of a boolean in my ViewModel changes... But I have no idea how to bind this.
I have this view:
<EditText
android:id="#+id/help_search_et"
android:layout_width="48dp"
android:layout_height="match_parent"
android:background="#drawable/border_search_help_txt"
android:hint="#string/hint_how_can_we_help"
android:singleLine="true" />
And a view model:
public class SomethingViewModel extends BaseViewModel {
private boolean isSearchEnabled;
void handleSearchRequest(){
if(isSearchEnabled) {
isSearchEnabled = false;
/* I need to make the EditText expand */
} else {
isSearchEnabled = true;
/* I need to make the EditText colapse */
}
}
}
As I am using the MVVM, I cannot have a reference of the View... so I cannot trigger an animation in the view, I need the Databinding to do that for me... But I don't know how to trigger this animation in my view.
I don't need help with width animation, just the data binding part.
There are several ways to do this. The first that comes to mind is to use Transitions:
binding.addOnRebindCallback(new OnRebindCallback() {
#Override
public boolean onPreBind(ViewDataBinding binding) {
TransitionManager.beginDelayedTransition(
(ViewGroup)binding.getRoot());
return super.onPreBind(binding);
}
});
Another alternative is to create a BindingAdapter:
#BindingAdapter("isExpanded")
public void setExpanded(View view, boolean isExpanded) {
// expand or collapse View, depending on isExpanded
}
And you'd have to bind it in your layout:
<View app:isExpanded="#{viewModel.isSearchEnabled}" .../>

Swipe between filtered images for android

Essentially, I am re-asking this question but for implementing it on android.
I am trying to allow users to swipe between filters on a static image.
The idea is that the image stays in place while the filter scrolls
above it. Snapchat recently released a version which implements this
feature. This video shows exactly what I'm trying to accomplish at
1:05.
I tried filling a list with the overlays and paging through it with the onFling and drawing with onDraw, but I lose the animations. Is there a way this can be done with ViewPager?
EDIT: As requested, I have provided my implementation for overlay view paging. It fills the viewpager with transparent png images which sits on top of an image view. Also, this code is in C#, as I am using Xamarin Android. It's fairly similar to Java for those unfamiliar with C#
...
static List<ImageView> overlayList = new List<ImageView>();
...
public class OverlayFragmentAdapter : FragmentPagerAdapter
{
public OverlayFragmentAdapter(Android.Support.V4.App.FragmentManager fm) : base(fm)
{
}
public override int Count
{
get { return 5; } //hardcoded temporarily
}
public override Android.Support.V4.App.Fragment GetItem(int position)
{
return new OverlayFragment ();
}
}
public class OverlayFragment : Android.Support.V4.App.Fragment
{
public override View OnCreateView (LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View view = inflater.Inflate (Resource.Layout.fragment_overlay, container, false);
LinearLayout l1 = view.FindViewById<LinearLayout> (Resource.Id.overlay_container);
ImageView im = new ImageView (Activity);
im.SetImageResource (Resource.Drawable.Overlay); //Resource.Drawable.Overlay is a simple png transparency I created. R
l1.AddView (im);
overlayList.AddElement (im);
return view;
}
}
Activity Layout XML:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="bottom">
<ImageView
android:id="#+id/background_image"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<RelativeLayout <!-- This second layout is for buttons which I have omitted from this code -->
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:id="#+id/edit_layout">
<android.support.v4.view.ViewPager
android:id="#+id/overlay_pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
</RelativeLayout>
Fragment Overlay XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/overlay_container"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center" />
To briefly summarize: the viewpager sits on top of the first imageview, which acts as a background. The OnCreateView method creates an overlay fragment and an overlay imageview from a resource, which it puts inside the overlay_container layout. Saving the image (Which I have not posted as it is outside the scope of this question) is simple, all it does is create a background bitmap, an overlay bitmap, and uses a canvas to draw the overlay onto the background, then writes to file.
I've worked on something similar myself.
For your specific use case, I would just use a canvas and alpha blend the filters across, on fling, as the top image.
To do the alpha blending, set the alpha paint of the first image (the original) to 255 and the alpha of the second one (the filter) to something like 128.
You just need a filter with the size of the image and then you shift the position of the second image as you draw it. That's it.
It's extremely fast and works a treat on very, very old devices.
Here's a sample implementation:
Bitmap filter, // the filter
original, // our original
tempBitmap; // the bitmap which holds the canvas results
// and is then drawn to the imageView
Canvas mCanvas; // our canvas
int x = 0; // The x coordinate of the filter. This variable will be manipulated
// in either onFling or onScroll.
void draw() {
// clear canvas
mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
// setup paint
paint0.setAlpha(255); // the original needs to be fully visible
paint1.setAlpha(128); // the filter should be alpha blended into the original.
// enable AA for paint
// filter image
paint1.setAntiAlias(true);
paint1.setFlags(Paint.ANTI_ALIAS_FLAG); // Apply AA to the image. Optional.
paint1.setFlags(Paint.FILTER_BITMAP_FLAG); // In case you scale your image, apple
// bilinear filtering. Optional.
// original image
paint0.setAntiAlias(true);
paint0.setFlags(Paint.ANTI_ALIAS_FLAG);
paint0.setFlags(Paint.FILTER_BITMAP_FLAG);
// draw onto the canvas
mCanvas.save();
mCanvas.drawBitmap(original, 0,0,paint0);
mCanvas.drawBitmap(filter, x,0,paint1);
mCanvas.restore();
// set the new image
imageView.setImageDrawable(new BitmapDrawable(getResources(), tempBitmap));
}
And here are basic onFling and onScroll implementations.
private static final int SWIPE_DISTANCE_THRESHOLD = 125;
private static final int SWIPE_VELOCITY_THRESHOLD = 75;
// make sure to have implemented GestureDetector.OnGestureListener for these to work.
#Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
float distanceX = e2.getX() - e1.getX();
float distanceY = e2.getY() - e1.getY();
if (Math.abs(distanceX) > Math.abs(distanceY) && Math.abs(distanceX) >
SWIPE_DISTANCE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
// change picture to
if (distanceX > 0) {
// start left increment
}
else { // the left
// start right increment
}
}
}
#Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// checks if we're touching for more than 2f. I like to have this implemented, to prevent
// jerky image motion, when not really moving my finger, but still touching. Optional.
if (Math.abs(distanceY) > 2 || Math.abs(distanceX) > 2) {
if(Math.abs(distanceX) > Math.abs(distanceY)) {
// move the filter left or right
}
}
}
Note: The onScroll/onFling implementations have pseudo code for the x adjustments, as those functions need to be tested. Someone who ends up implementing this in the future, can feel free to edit the answer and provide those functions.
Take a look in the implementation of the method onDraw for the default Calendar app: DayView.
There is onFling implementation and redrawing of the content (for example, calendar grid) according to the motion changes, which imitates fling.
Then you can use ColorFilter in onDraw according to the motion changes. It is very fast.
Alternatively, you can use ViewSwitcher with a list of filtered images (or somehow created a filtered images cache). To achieve the possibility of "drawing over the image", you can use ImageView and ViewSwitcher in RelativeLayout one above another and set the new filtered image in ImageView after the end of scrolling.
For this application i feel it would be most easy to use androids animation features and set the animations value to the filter you want. So you would make your own animation the changed filters iterating over your array.

LayoutParams won't properly put ImageViews next to each other

Currently, I'm trying to make an infinitely long chain of ImageViews that are all next to each other.
I attempt to do this by using addRule and LayoutParam so that every new ImageView has to be set to the right of the ImageView before it.
However, I encounter an odd problem. When I do run my code, this is the result :
Not only are my two ImageViews not even next to each other, it doesn't work for any number past two! If I just create two blocks, it gives the result below.
If I make any more blocks than 2, they'll just stack on top of block #2. Which is what happens in the image above.
Also, I have to set every ImageView with its own ID because if I don't, they'll all have an ID of -1.
I've been stuck on this for like 5 hours in a row, and it's really starting to get annoying. (Please do not suggest that I use a LinearLayout. I already know, and I can't use it.)
public class TerrainFactory {
int count = 0;
int idCount = 1;
Terrain terrain;
public Terrain createNewTerrain(Activity activity, RelativeLayout relativeLayout,
final ArrayList<Terrain> terrainArrayList){
//TODO: Add the rectangle bounds into this.
terrain = new Terrain();
terrain.is = activity.getResources().openRawResource(+R.drawable.game_tile);
terrain.tile = BitmapFactory.decodeStream(terrain.is);
terrain.tileDrawable = new BitmapDrawable(activity.getResources(), terrain.tile);
terrain.terrainImage = new ImageView(activity);
terrain.terrainImage.setImageDrawable(terrain.tileDrawable);
//noinspection ResourceType
terrain.terrainImage.setId(idCount);
idCount++;
if(count >= 1) {
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);;
layoutParams.addRule(RelativeLayout.RIGHT_OF, terrainArrayList.get(count - 1).terrainImage.getId());
relativeLayout.addView(terrain.terrainImage, layoutParams);
terrain.terrainImage.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
public void onGlobalLayout() {
//TODO: Set rectangle bounds in here
++count;
terrain.terrainImage.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
}else{
++count;
relativeLayout.addView(terrain.terrainImage);
terrain.terrainImage.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
public void onGlobalLayout() {
//TODO: Set rectangle bounds in here
terrain.terrainImage.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
}
return terrain;
}
public int getCount() { return count; }
}
Here is my PlayActivity class, where all the block creating happens :
public class PlayActivity extends Activity {
RelativeLayout relativeLayout;
TerrainFactory terrainFactory;
ArrayList<Terrain> terrainArrayList = new ArrayList<Terrain>();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_play);
relativeLayout = (RelativeLayout) findViewById(R.id.relativeLayout);
terrainFactory = new TerrainFactory();
for(int i = 0; i < 7; i++){
terrainArrayList.add(terrainFactory.createNewTerrain(PlayActivity.this, relativeLayout, terrainArrayList));
}
}
}
And here is the Terrain class
public class Terrain {
public Bitmap tile;
public InputStream is;
public ObjectAnimator moveLeft;
public Drawable tileDrawable;
public Rect bounds;
public ImageView terrainImage;
}
Here's the XML for the RelativeLayout. The RelativeLayout I'm using is the one nested inside the RelativeLayout that fills the entire screen.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.hooray.project.fun.activity.PlayActivity">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="false"
android:id="#+id/relativeLayout"></RelativeLayout>
</RelativeLayout>
Basically I jsut would tackle this in another way....using not Views for buildiung terrains but using surfaceview and draw bitmaps on it...
But well lets do it your way:
Place your views into a FrameLayout. Then when you create your views give them their dimension in code and also set the x and y positions in code.
For example this is how I give a View the needed Dimension and the position in my Code. Its a Textview and I jsut set the x and y position which I have stuffed iinto variables so I can chnage them how i want:
tView.setLayoutParams(new FrameLayout.LayoutParams(hintListWidth,hintListHeight));
tView.setX(hintListPosX);
tView.setY(hintListPosY);
maybe for your example do it like this:
float startPosX=???;
float startPosY=???;
float i=0;
for(Terrian t:terrainArrayList)
{
tView.setLayoutParams(new FrameLayout.LayoutParams(width,height));
tView.setX(startPosX+width*i);
tView.setY(startPosY);
i++;
}
If you have the width and height ready and know where to start placing the first view every view will be placed side by side from left to right...
As suggested in the other answer(s) to your question, the problem is that counter on the if-true branch is never incremented before views are drawn (aka tree observer callback being called). You are calling addView but I think this goes on and adds the view asynchronously, while you go on and add views. So you finish adding the views and only after that they are drawn. I suggest taking the counter out of the callback and place it at the end of the if-true block.
Note: if you are targeting SDK17+ you can use View.generateViewId() instead of manually keeping track / incrementing it yourself. Not that it matters in this particular scenario, but it will not allow the generated id to conflict with aapt-generated ids and it will roll it over once the top limit is reached.

Categories