Efficiently Inflating a lot of Views within several Horizontal LinearLayout with Adapters

旧时模样 提交于 2021-02-04 20:04:48

问题


I have a question about how to improve the performance of a large horizontal linear layout.

I am creating a table like view that can have anywhere from 50 to 2,500 entries. Each entry is a LinearLayout containing a TextView with some simple text. I have implemented the design by utilizing the LinearListView library. This library allows an ListAdapter to be bound to a LinearLayout to display a view in a horizontal or vertical orientation.

The way I have implemented this currently is by utilizing two of these LinearListViews. One is vertical who data consists of horizontal LinearListViews. This gives the desired output by creating a table view. I then wrap the table within a Horizontal ScrollView with a Verticle ScrollView so that the table can be panned (scrolled in either up/down or left/right).

The issue with this layout is that the adapters getView() is called only at the initialization of the views (the first time it inflates the first linearList it inflates every view). Once each view is inflated, the getView() method is never called again while scrolling through the list (it scrolls very well of course as it is all loaded). The total amount of time it takes to inflate is not as big of deal, but the fact that it inflates every item in the table in a row locks up the main UI thread. I need the view to "lazy load" each item in the table without blocking the UI thread.

I have a screen shot, but I do not have enough reputation to post it nor do I have enough to use two links. I will attempt to put a external photo link in the comments. (In reference to the screenshot) The table data is generated in a group of async tasks where each table item is the current time in Milliseconds (I am aware that the table rows are not ordered due to the async nature and will fix that later). This actual app serves no purpose other than to demonstrate this library.

I added the "Change Data Randomly" button which will create a random point (int x,int y) and generate a random string and replace the cell at (x,y) with that string. This happens almost instantly by calling that particulars adapter's getView() method. So access to this table is very quick! Again, it is the initial inflating that is locking the main UI thread.

A few important notes to summarize:

  • The UI needs to be in a table format where each row could have different lengths and can dynamically be changed. Items can be removed and added from any part.
  • My getView() methods (2 adapters & 2 LinearListViews) are utilizing the ViewHolder pattern and are fairly optimized.
  • My main goal is not to necessarily improve the total speed, but to efficiently load each view so that the main UI thread is not locked up (I want to still be able to use the interface while the table loads).
  • Consider the content of each cell to be a textview for simplicity (Images and other complex components can be supported later).
  • The table needs to be able to scroll in any direction (it is not possible to display all of its contents on the screen at one time).

I have found this application (TV SideView) which creates a fairly large table view that loads really nicely. I ultimately would want to achieve something similar to this (look at the "Program Guide" page to see the actual table). It loads a bunch of cells and you can still use the UI (drag the table around when it first opens and you will see the cells loading).

I will keep chugging away at this and post back anything new I find.

Any advice and help would be greatly appreciated! Thank you so much for your time!

-Evan


回答1:


Since you're concerned with the UI performance, you can use AsyncTask abstract class, common use and recommended by Google. AsyncTask runs on a separate thread from the UI. To use it, you must create a class to subclass it. Google webpage @ AsyncTask, for your convenience.

Code samples I found is at Using an AsyncTask to populate a ListView. In the code, notice getItemLists extends AsyncTask. Override onPostExecute() in that class calls setListAdapter method, which you are probably familiar with.

Code snippets from the link above:

private class getItemLists extends
            AsyncTask<Void, String, ArrayList<Item>> {
...
   @Override
   protected String doInBackground(String... params) {
   // Good place to add code for time consuming work, no UI access though.
   ...
   }

   @Override
   protected void onPostExecute(ArrayList<Item> result) {
   super.onPostExecute(result);
   ...
   }

I never had to use this AsyncTask but I might. Please keep us posted on this. Good luck...




回答2:


I have figured it out :)

@TheOriginalAndroid answer is an excellent idea and response! Thank you so much for your time and help. I actually had already started implementing an AsyncTask Manager and finished it yesterday morning.

I solved this by creating a class called AsycnGridManager that will manage the group of asyncTasks responsible for painting the view. It is quite a bit of code, but I went into great detail in the comments. This is not the actual code but a shell to show an overview of how it works. I have not compiled it so please don't take it as a diamond. This class should be created and started from the main thread within your main activity or fragment that is responsible for it.

/**
 * This class will manage a view and load it asynchronously.
 * In particular, this view will manage a linearLayout in 
 * 2D space. IE. One verticle linear layout with a horizontal 
 * linearLayout at each row.
 * @author Evan Boucher
 */
public class AsyncGridManager {

    /**
     * This is the core number of Threads in the pool.
     * You should probably consider checking the 
     * system for the number of cores the device has.
     * I currently use 4 as it fits my needs.
     */
    private static final int NUM_OF_THREADS_IN_POOL = 4;

    /**
     * The max number of threads that can exist in the pool at one time.
     */
    private static final int MAX_NUM_OF_THREADS_IN_POOL = 10;

    /**
     * The max number of tasks that the queue can hold for the 
     * pool
     */
    private static final int MAX_NUM_OF_TASKS_IN_QUEUE = 150;

    /**
     * The max keep alive time for a thread task in the pool.
     * This should be longer than your longest task. If you have
     * a long UI task in each thread (you are probably doing
     * to much to begin with!) then the task may get stopped
     * before it finishes.
     */
    private static final int THREAD_KEEP_ALIVE_TIME = 4000;

    /**
     * The minimum time to wait to paint a single EPG item.
     * This means that a block will never be painted any faster
     * than this number in Milliseconds.
     */
    private final int MIN_WAIT_TIME_TO_PAINT = 100;

    /**
     * The max time an async task will sleep before painting on the
     * UI thread.
     */
    private final int MAX_WAIT_TIME_TO_PAINT = 1000;

    /**
     * The thread pool that the async tasks within this class will
     * pull from. This is defined by the above varaibles.
     */
    private ThreadPoolExecutor mThreadPool;

    /**
     * The queue of tasks that the thread pool will pull from.
     * The size is fairly large as I don't much care about memory 
     * usage right now. Once the queue fills up it will not add
     * anymore tasks. Be aware of that! So tasks can be lost or
     * cause a thread to block (if you add the tasks on the main
     * thread).
     */
    private BlockingQueue taskQueue;

    /**
     * The thread that this manager will run on as to not block the main thread.
     */
    public Thread mGridManagerThread;

    /**
     * The Grid map object that is the underlying data for this grid.
     * Each key is a row and each value is a list for the columns in that
     * row.
     */
    private Map<String,List<CustomObject>> mGridMap;
    //Number of rows in the table (size of the mGridMap)
    private int mNumOfRows;
    //Get the rootView that is already inflated. This is what we will add to.
    private LinearLayout mRootGridView;
    //The Android activity context that this special async manager is attached to.
    private Context mContext;

    /**
     * Creates and initializes this class.
     *
     */
    public AsyncGridManager(Context context, LinearLayout rootView, Map<String,List<CustomObject>> gridMap) {

        //Create a new taskqueue for the EPGblocks.
        taskQueue = new ArrayBlockingQueue<CreateEPGTableRowTask>(MAX_NUM_OF_TASKS_IN_QUEUE);

        //Create a new threadpool for the tasks.
        poolExecutor = new ThreadPoolExecutor(NUM_OF_THREADS_IN_POOL, MAX_NUM_OF_THREADS_IN_POOL, THREAD_KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS, taskQueue);
        this.mGridMap = gridMap;
        /*
         * We can only get the number of rows as that is predefined 
         * by this datastructure (won't know how many columns until we get to the row).
         */
        this.mNumOfRows = mGridMap.size();

        this.mContext = context;
        /*
         * The RootView should be a LinearLayout in my case and already be inflated!
         */
        this.mRootGridView = rootView
    }
    /**
     * Tell the async manager to start loading the tasks into the queue.
     * It loads on a seperate thread to make this completely async.
     */
    public void startAsyncLoading() {
        /*
         * It is important here to note that we should inflate the mRootGridView
         * This way adding views to it will be async on the UI thread.
         */
        mGridManagerThread = new Thread(new AsyncGridLoaderRunnable());
        mGridManagerThread.start();
    }

    /**
     * The runnable for this manager to generate 
     */
    public class AsyncGridLoaderRunnable extends Runnable {

        @Override
        public void run() {
            //A for loop to go through the size of the rows 
            for (int i = 0; i < mNumOfRows; i++) {
                //For each row, lets make a AsyncTask to generate and paint that row. You need to make a new one everytime.
                CreateRowAsyncTask rowAsyncTask = new CreateRowAsyncTask(i);
                /*
                 * I pass i in here so that you could also get the rowIndex as a parameter too if we want.
                 * This adds the task to the taskQueue for this pool to execute.
                 */
                rowAsyncTask.executeOnExecutor(poolExecutor, i);
            }
        }
    }
    /**
     * Async Task that will create and print a row
     * from the map.
     */
    public class CreateRowAsyncTask extends AsyncTask {
        //Random generator to force tasks to sleep for random periods.
        private Random mRandomGenerator;
        //The row index that this task is responsible for painting and managing.
        private int rowIndex;
        //The horizontal linearlayou that represents this row. Might want to add it to a list so we can reference it later.
        private LinearLayout singleRowLayout;

        //The local reference to the list of columns for this row.
        private List<CustomObject> columnList;

        public CreateRowAsyncTask(int rowIndex) {
            this.mRandomGenerator = new Random();
            this.rowIndex = rowIndex;
            //Create the linearlayout for the row.
            singleRowLayout = new LinearLayout(mContext);
            //Set it to horisontal to be a row.
            singleRowLayout.setOrientation(LinearLayout.HORIZONTAL);
            //Get a reference to this rows list of columns.
            columnList = mGridMap.get(rowIndex);
        }
        @Override
        protected Object doInBackground(Object... arg0) {
            /*
             * Here you could do some background stuff to setup objects /views.
             * I am going to assume you have some method to generate the view
             * from our CustomObject (the items within the list for the rows).
             */
            //Lets tell the UI thread to add our row real quickly (remember the root view was already inflated)
            mRootGridView.addView(singleRowLayout);

            /*
             * Due to the Async nature we need to draw each row together.
             * If we dont, EPG blocks will be out of order (not guaranteed order).
             * Uses onProgressUpdate() to paint each block in the row.
             */
            CustomObject columnObject;
            for (int i = 0; i < columnList.size(); i++) {
            //Lets save a reference to the object we want to add to the row we are on
            columnObject = columnList.get(i);

                /*
                 * The customView we are adding. This assumes that the columnObject createView() method
                 * will create a new LinearLayout (or View of some type) which we will add to this row.
                 * You could put the createView() call directly in the publishProgress() method for
                 * ease, but I left it out to show the custom view creation.
                 * Be sure that the createView() does not handle any inflated views (these must be 
                 * accessed on the UI thread).
                 */
                CustomView newViewToAddAsColumn = columnObject.createView();
                //Create each row and use ProgressUpdate to paint it.
                publishProgress(newViewToAddAsColumn);
                try {
                    /*
                     * Sleep the task for a random period of time, this way the view is not loading all at once.
                     * This is one strategy, there are plenty of other Async Loading strategies
                     */
                    Thread.sleep(mRandomGenerator.nextInt(MAX_WAIT_TIME_TO_PAINT - MIN_WAIT_TIME_TO_PAINT) + MIN_WAIT_TIME_TO_PAINT);

                } catch (InterruptedException e) {
                    Log.e(TAG, "ERROR! AsyncTask failed to wait!!!");
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }

        @Override
        protected void onProgressUpdate(Object... values) {
            //Get the customView and add it to the row.
            CustomView customViewToAdd = (EpgEventView) values[0];
            //Add the customView to the row. We assume that the params for the view are within the customView.
            singleRowLayout.addView(customViewToAdd, customViewToAdd.getParams());
        }



     }

   }

I have not run this code specifically so use it more as an example than a perfect solution. This code will add views asynchronously to the rootView without blocking the UI experience. :)

Enjoy,

-Evan




回答3:


I was able to do this when working with accordion style layout where each collapsing item hold also a subset of list as that kind of layout is not working well with RecyclerView Viewholder pattern. I used Concurrent as a replacement for Asynctask then on doInBackground part all Glide fetch for images and addView() call was wrapped using new Handler(Looper.getMainLooper()).post(() -> {//Your Code}); since Glide and adding view requires it to be run on UI thread. The adding of each layout will be seen one by one in the screen but the good thing is no more frame skip from Choreographer.

This is my code looks like

public class GenerateLayoutAsync extends BaseConCurrentTask<Object> {

    private final WeakReference<FragmentActivity> activityReference;
    private final LinearLayoutCompat linearLayoutCompat;
    private final List<GroupMatch> groupMatchList;

    public GenerateLayoutAsync(FragmentActivity context, LinearLayoutCompat linearLayoutCompat, List<GroupMatch> groupMatchList) {
        this.activityReference = new WeakReference<>(context);
        this.linearLayoutCompat = linearLayoutCompat;
        this.groupMatchList = groupMatchList;
    }

    @Override
    public void setUiForLoading() {


    }

    @Override
    public Object call() {

        for (int i = 0; i < groupMatchList.size(); i++) {

            GroupMatch groupMatch = groupMatchList.get(i);

            AppCompatTextView title;
            LinearLayoutCompat container;

            View itemView = LayoutInflater.from(this.activityReference.get()).inflate(R.layout.group_card, linearLayoutCompat, false);

            title = itemView.findViewById(R.id.groupTitle);
            container = itemView.findViewById(R.id.populateView);

            title.setText(groupMatch.getTitle());
            container.setVisibility(View.GONE);
            title.setOnClickListener(v -> {
                if (container.getVisibility() == View.VISIBLE)
                    container.setVisibility(View.GONE);
                else
                    container.setVisibility(View.VISIBLE);
            });

            for (int j = 0; j < groupMatch.getModelList().size(); j++) {

                MatchModel matchModel = groupMatch.getModelList().get(j);

                AppCompatTextView home, away, middleText, topText, bottomText, betBtn;
                AppCompatImageView shareBtn, homeFlag, awayFlag;

                View view = LayoutInflater.from(this.activityReference.get()).inflate(R.layout.match_card, (ViewGroup) itemView, false);

                home = view.findViewById(R.id.homeTeam);
                away = view.findViewById(R.id.awayTeam);
                topText = view.findViewById(R.id.topTextV);
                middleText = view.findViewById(R.id.middleTextV);
                bottomText = view.findViewById(R.id.bottomTextV);
                betBtn = view.findViewById(R.id.betNowBtn);
                shareBtn = view.findViewById(R.id.shareBtn);
                homeFlag = view.findViewById(R.id.homeFlag);
                awayFlag = view.findViewById(R.id.awayFlag);

                if (CampaignModel.isIsTarget() && CampaignModel.isFetchAds()) {
                    betBtn.setVisibility(View.VISIBLE);
                    betBtn.setOnClickListener(v -> this.activityReference.get().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(CampaignModel.getREDIRECT()))));
                }
                else
                    betBtn.setVisibility(View.GONE);

                home.setText(matchModel.getHome());
                away.setText(matchModel.getAway());

                home.setSelected(true);
                away.setSelected(true);

                LocalDateTime localDateTime;

                if (matchModel.getHomeScore().isEmpty() && matchModel.getAwayScore().isEmpty()){
                    betBtn.setAlpha(1f);
                    betBtn.setEnabled(true);
                    localDateTime = LocalDateTime.parse(matchModel.getStartDate(), DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm"));
                    String date = localDateTime.format(DateTimeFormatter.ofPattern("MM/dd/yy"));
                    String time = localDateTime.format(DateTimeFormatter.ofPattern("HH:mm a"));
                    topText.setText(time);
                    bottomText.setText(date);
                    middleText.setText(null);
                }
                else{
                    betBtn.setAlpha(0.3f);
                    betBtn.setEnabled(false);
                    localDateTime = LocalDateTime.parse(matchModel.getEndDate(), DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm"));
                    String date = localDateTime.format(DateTimeFormatter.ofPattern("MM/dd/yy"));
                    topText.setText(matchModel.getHomeScore());
                    bottomText.setText(matchModel.getAwayScore());
                    middleText.setText(date);
                }

                new Handler(Looper.getMainLooper()).post(() -> {
                    Glide.with(this.activityReference.get())
                            .asDrawable()
                            .load(matchModel.getHomeFlag())
                            .error(R.drawable.ic_flag)
                            .into(homeFlag);

                    Glide.with(this.activityReference.get())
                            .load(matchModel.getAwayFlag())
                            .error(R.drawable.ic_flag)
                            .into(awayFlag);
                });

                shareBtn.setOnClickListener(v -> {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                        if (ContextCompat.checkSelfPermission(this.activityReference.get(), Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(this.activityReference.get(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)
                            ShareToSocial.saveAndShare(matchModel.getHome() + " vs " + matchModel.getAway(), itemView);
                        else
                            this.activityReference.get().requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
                    }
                    else
                        ShareToSocial.saveAndShare(matchModel.getHome() + " vs " + matchModel.getAway(), itemView);
                });

                new Handler(Looper.getMainLooper()).post(() -> container.addView(view));

            }

            new Handler(Looper.getMainLooper()).post(() -> linearLayoutCompat.addView(itemView));

        }

        return null;
    }

    @Override
    public void setDataAfterLoading(Object result) {


    }

}

As you can see I am adding views on each header layout in the second for loop then add each header layout to the main layout in the end of the first for loop.

If you are not familiar yet with Concurrent util of Java you may also use the old AsyncTask in the same approach.



来源:https://stackoverflow.com/questions/29018811/efficiently-inflating-a-lot-of-views-within-several-horizontal-linearlayout-with

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!