使用适配器有效地在多个水平LinearLayout中充气很多视图

时间:2015-03-12 19:39:33

标签: android performance android-layout android-linearlayout android-adapter

我有一个关于如何提高大型水平线性布局性能的问题。

我正在创建一个类似于视图的表,可以包含50到2,500个条目。每个条目都是一个LinearLayout,其中包含带有一些简单文本的TextView。我使用LinearListView library实现了设计。此库允许将ListAdapter绑定到LinearLayout以显示水平或垂直方向的视图。

我目前实现此方法的方法是使用其中两个LinearListViews。一个是垂直的,数据由水平LinearListViews组成。这通过创建表视图来提供所需的输出。然后,我使用Verticle ScrollView将表格包装在水平ScrollView中,以便可以平移表格(向上/向下或向左/向右滚动)。

此布局的问题在于,仅在视图初始化时调用适配器getView()(第一次膨胀第一个linearList时,它会为每个视图充气)。一旦每个视图都膨胀,在滚动列表时永远不会再调用getView()方法(当然它滚动得非常好,因为它全部被加载)。膨胀所花费的总时间并不是很大,但是它连续膨胀表中每个项目的事实会锁定主UI线程。我需要查看"延迟加载"表中的每个项目都没有阻止UI线程。

我有一个屏幕截图,但我没有足够的声誉发布它,也没有足够的使用两个链接。我将尝试在评论中添加外部照片链接。 (参考屏幕截图)表数据是在一组异步任务中生成的,其中每个表项都是以毫秒为单位的当前时间(我知道由于异步性质而没有对表行进行排序,以后会修复它) 。除了演示此库之外,这个实际的应用程序没有任何用途。

我添加了"随机更改数据"按钮,它将创建一个随机点(int x,int y)并生成一个随机字符串,并用该字符串替换(x,y)处的单元格。这通过调用特定的适配器的getView()方法几乎立即发生。所以访问这个表非常快!同样,它是锁定主UI线程的初始膨胀。

总结一些重要的注释:

  • UI需要采用表格格式,其中每行可以具有不同的长度并且可以动态地更改。可以从任何部分删除和添加项目。
  • 我的getView()方法(2个适配器和2个LinearListViews)正在使用ViewHolder模式并且相当优化。
  • 我的主要目标不是必须提高总速度,而是有效地加载每个视图,以便主UI线程没有被锁定(我希望在表加载时仍能使用该接口)。
  • 为简单起见,请将每个单元格的内容视为textview(稍后可以支持图像和其他复杂组件)。
  • 桌子需要能够向任何方向滚动(不可能一次在屏幕上显示所有内容)。

我发现这个application (TV SideView)创建了一个相当大的表视图,可以非常好地加载。我最终希望实现与此类似的功能(查看"程序指南"页面以查看实际表格)。它加载了一堆单元格,你仍然可以使用UI(在第一次打开时拖动桌子,你会看到加载的单元格)。

我会继续嘲笑这个并发回我找到的任何新内容。

非常感谢任何建议和帮助!非常感谢你的时间!

-Evan

3 个答案:

答案 0 :(得分:0)

由于您关注UI性能,您可以使用AsyncTask抽象类,常用和Google推荐。 AsyncTask在与UI不同的线程上运行。要使用它,您必须创建一个类来对其进行子类化。谷歌网页@ AsyncTask,为了您的方便。

我找到的代码示例位于Using an AsyncTask to populate a ListView。 在代码中,请注意getItemLists扩展了AsyncTask。在该类中覆盖 onPostExecute ()会调用您可能熟悉的 setListAdapter 方法。

以上链接的代码段:

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);
   ...
   }

我从来没有使用过这个AsyncTask,但我可能会这样做。请留意我们。祝你好运......

答案 1 :(得分:0)

我已经弄明白了:)

@TheOriginalAndroid答案是一个很好的主意和回应!非常感谢你的时间和帮助。我实际上已经开始实现AsyncTask Manager并在昨天早上完成了它。

我通过创建一个名为AsycnGridManager的类来解决这个问题,该类将管理负责绘制视图的asyncTasks组。这是相当多的代码,但我在评论中详细介绍了。这不是实际的代码,而是一个shell,用于显示其工作原理的概述。我没有编辑它所以请不要把它作为钻石。应该从主要活动中的主线程或负责它的片段创建和启动此类。

/**
 * 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());
        }



     }

   }

我没有专门运行此代码,因此将它作为一个例子而不是一个完美的解决方案。此代码将异步添加视图到rootView,而不会阻止UI体验。 :)

享受,

-Evan

答案 2 :(得分:0)

在使用手风琴样式布局时,我能够做到这一点,其中每个折叠项也包含列表的一个子集,因为这种布局不适用于 RecyclerView Viewholder 模式。我使用 Concurrent 作为 Asynctask 的替代品,然后在 doInBackground 部分,所有 Glide 获取图像和 addView() 调用都使用 new Handler(Looper.getMainLooper()).post(() -> {//Your Code}); 包装,因为 Glide 和添加视图要求它在 UI 线程上运行。每个布局的添加都会在屏幕上一一看到,但好在 Choreographer 不再跳帧。

这是我的代码看起来像

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) {


    }

}

如您所见,我在第二个 for 循环中为每个标题布局添加视图,然后在第一个 for 循环末尾将每个标题布局添加到主布局。

如果您还不熟悉 Java 的 Concurrent util,您也可以以相同的方式使用旧的 AsyncTask。