Android 使用适配器在多个水平线性布局中有效地扩展大量视图

Android 使用适配器在多个水平线性布局中有效地扩展大量视图,android,performance,android-layout,android-linearlayout,android-adapter,Android,Performance,Android Layout,Android Linearlayout,Android Adapter,我有一个关于如何提高大型水平线性布局的性能的问题 我正在创建一个类似于表的视图,它可以包含50到2500个条目。每个条目都是一个线性布局,包含一个包含一些简单文本的TextView。我已经通过使用实现了设计。此库允许ListAdapter绑定到LinearLayout,以水平或垂直方向显示视图 目前我实现这一点的方法是利用其中两个LinearListView。一种是垂直的who数据,由水平的线性视图组成。这将通过创建表视图提供所需的输出。然后,我用垂直滚动视图将表格包装在水平滚动视图中,以便可以

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

我正在创建一个类似于表的视图,它可以包含50到2500个条目。每个条目都是一个线性布局,包含一个包含一些简单文本的TextView。我已经通过使用实现了设计。此库允许ListAdapter绑定到LinearLayout,以水平或垂直方向显示视图

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

此布局的问题在于,仅在视图初始化时调用adapters getView()(第一次对第一个线性列表进行充气时,会对每个视图进行充气)。一旦每个视图都膨胀,在滚动列表时就不会再次调用getView()方法(当然,它滚动得非常好,因为它已全部加载)。膨胀所需的总时间并没有什么大不了的,但事实上,它膨胀表中的每一项,一行锁定了主UI线程。我需要视图在不阻塞UI线程的情况下“延迟加载”表中的每个项

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

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

需要总结的几个重要注意事项:

  • UI需要采用表格格式,其中每行可以具有不同的长度,并且可以动态更改。可以从任何零件中删除和添加项目
  • My getView()方法(2个适配器和2个LinearListView)利用了ViewHolder模式,并且经过了相当优化
  • 我的主要目标不是一定要提高总速度,而是高效地加载每个视图,以便主UI线程不会被锁定(我希望在加载表时仍然能够使用该接口)
  • 为了简单起见,将每个单元格的内容视为文本视图(稍后可以支持图像和其他复杂组件)
  • 表格需要能够向任何方向滚动(不可能一次在屏幕上显示所有内容)
我发现它创建了一个相当大的表视图,可以很好地加载。我最终希望实现类似的目标(查看“节目指南”页面以查看实际的表格)。它加载了一堆单元格,您仍然可以使用UI(当表格第一次打开时拖动表格,您将看到单元格正在加载)

我会继续努力,把我发现的任何新东西都发回

任何建议和帮助都将不胜感激!非常感谢您抽出时间


-Evan

因为您关心UI性能,所以可以使用AsyncTask抽象类,这是Google推荐的常用类。AsyncTask在UI的单独线程上运行。要使用它,必须创建一个类来对其进行子类化。谷歌网页@,为了您的方便

我找到的代码示例位于。 在代码中,请注意GetItemList扩展了AsyncTask。该类中的OverrideonPostExecute()调用您可能熟悉的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);
   ...
   }
私有类getItemList扩展
异步任务{
...
@凌驾
受保护的字符串doInBackground(字符串…参数){
//很好的地方添加代码耗时的工作,但没有用户界面访问。
...
}
@凌驾
受保护的void onPostExecute(ArrayList结果){
super.onPostExecute(结果);
...
}
我从未使用过此任务,但我可能会使用。请随时向我们发布此任务。祝您好运…

我已经找到答案:)

@TheOriginalAndroid答案是一个非常好的想法和回应!非常感谢您的时间和帮助。事实上,我已经开始实现一个异步任务管理器,并于昨天上午完成了它

我通过创建一个名为AsycnGridManager的类来解决这个问题,该类将管理负责绘制视图的一组异步任务。这是一段相当多的代码,但我在注释中详细介绍了它。这不是实际的代码,而是一个展示其工作原理的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());
        }



     }

   }
/**
*此类将管理视图并异步加载它。
*特别是,此视图将管理中的线性布局
*二维空间,即一个垂直线性布局,一个水平布局
*每行的线性布局。
*@作者埃文·鲍彻
*/
公共类异步管理器{
/**
*这是池中线程的核心数。
*你应该考虑检查一下。
*设备拥有的核心数量的系统。
*我目前使用4,因为它适合我的需要。
*/
池中线程的私有静态final int NUM=4;
/**
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) {


    }

}