PinterestLikeAdapterView 瀑布流实现原理

  PinterestLikeAdapterView是github上开源的项目,实现了流畅的瀑布流功能,上个图看看:

 

 

       功能实现:

       1、瀑布流,可简单配置显示列数。

       2、下拉刷新。

       3、上拉加载更多。

我从github上刚下的代码目录:

 

貌似结构挺复杂,其实挺简单,按照功能需求分析下目录结构:

1、要实现这种特殊的瀑布流布局界面,要自定义个View,自定义View的包:huewu.pla.lib

      MutiColumnListView ----自定义的瀑布流View

      PLA_ListView    ------MutiColumnListView的父类,主要是对瀑布流的显示的方法。

      PLA_AbsListView  -----PLA_ListView 的父类,是自定义listview的基类,对绑定数据进行处理。

      PLA_AdapterView----自定义View的是数据适配器,PLA_AbsListView 的父类。

      PLA_HeaderViewListAdapter  -----  自定义View的头布局和尾布局的适配器,在PLA_AbsListView用到。

2、要实现上拉下拉功能,需要对MutiColumnListView进行重写,把头尾和各种手势处理加上:me.maxwin.view

      XListView-----对MutiColumnListView进行重写,把头尾和各种手势处理加上,项目中直接用到的类。

      XListViewFooter-----添加的尾布局。

      XListViewHeader-----添加的头布局。

3、这里瀑布流加载的是网络图片,图片下载显示工具包:com.example.android.bitmapfun.util

      bitmapfun是google教程提供的工具包,是对图片的异步加载、缓存的工具,使用很简单,就一个方法把图片url和imageview控件传进去就可以。其显现逻辑感兴趣的可以学一下。

4、由于瀑布流图片显示的自动调整宽高,就要对imageview进行重写:com/dodowaterfall/widget/ScaleImageView.java

     widget包里的另外还有两个自定义View不知道干啥使得,项目中也没用到,暂时先不管了。

5、工具包和自定义view准备好,调用自己界面显示瀑布流:com/huewu/pla/sample/PullToRefreshSampleActivity.java

      图片数据显示首先通过网络获得图片的数据,图片数据的实体类包:com.dodola.model

再按照实现流程分析下,瀑布流功能的实现逻辑:

1、获取网络图片数据,http+json流程没必要说,看下获得的数据:

 try {
                if (null != json) {
                    JSONObject newsObject = new JSONObject(json);
                    JSONObject jsonObject = newsObject.getJSONObject("data");
                    JSONArray blogsJson = jsonObject.getJSONArray("blogs");

                    for (int i = 0; i < blogsJson.length(); i++) {
                        JSONObject newsInfoLeftObject = blogsJson.getJSONObject(i);
                        DuitangInfo newsInfo1 = new DuitangInfo();
                        newsInfo1.setAlbid(newsInfoLeftObject.isNull("albid") ? "" : newsInfoLeftObject.getString("albid"));
                        newsInfo1.setIsrc(newsInfoLeftObject.isNull("isrc") ? "" : newsInfoLeftObject.getString("isrc"));
                        newsInfo1.setMsg(newsInfoLeftObject.isNull("msg") ? "" : newsInfoLeftObject.getString("msg"));
                        newsInfo1.setHeight(newsInfoLeftObject.getInt("iht"));
                        duitangs.add(newsInfo1);
                    }
                }
            } catch (JSONException e) {
                e.printStackTrace();
            }

获得id、isrc、msg这三个一看就知道是什么,iht这个是图片的高,刚看的时候,不知道具体是干啥用的,肯定是来控制图片的显示宽高。
2、自定义adapter对imageview设置数据:

  @Override
        public View getView(int position, View convertView, ViewGroup parent) {

            ViewHolder holder;
            DuitangInfo duitangInfo = mInfos.get(position);

            if (convertView == null) {
                LayoutInflater layoutInflator = LayoutInflater.from(parent.getContext());
                convertView = layoutInflator.inflate(R.layout.infos_list, null);
                holder = new ViewHolder();
                holder.imageView = (ScaleImageView) convertView.findViewById(R.id.news_pic);
                holder.contentView = (TextView) convertView.findViewById(R.id.news_title);
                convertView.setTag(holder);
            }

            holder = (ViewHolder) convertView.getTag();
            holder.imageView.setImageWidth(duitangInfo.getWidth());
            holder.imageView.setImageHeight(duitangInfo.getHeight());
            holder.contentView.setText(duitangInfo.getMsg());
            mImageFetcher.loadImage(duitangInfo.getIsrc(), holder.imageView);
            return convertView;
        }

这里设置了imageview 的图片资源的宽高,我们现在至少知道高是从网络获得的,宽哪来的:

	public int getWidth(){
		return 200;
	}

这里是固定的200,可以推测我们获取的高的值,是相对于宽200 的相对值。

3、那Imageview是的实际宽高是在自定义ScaleImageView里动态计算的:

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);

        /**
         * if both width and height are set scale width first. modify in future
         * if necessary
         */

        if (widthMode == MeasureSpec.EXACTLY || widthMode == MeasureSpec.AT_MOST) {
            scaleToWidth = true;
        } else if (heightMode == MeasureSpec.EXACTLY || heightMode == MeasureSpec.AT_MOST) {
            scaleToWidth = false;
        } else
            throw new IllegalStateException("width or height needs to be set to match_parent or a specific dimension");

        if (imageWidth == 0) {
            // nothing to measure
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            return;
        } else {
            if (scaleToWidth) {
                int iw = imageWidth;
                int ih = imageHeight;
                int heightC = width * ih / iw;
                if (height > 0)
                    if (heightC > height) {
                        // dont let hegiht be greater then set max
                        heightC = height;
                        width = heightC * iw / ih;
                    }

                this.setScaleType(ScaleType.CENTER_CROP);
                setMeasuredDimension(width, heightC);

            } else {
                // need to scale to height instead
                int marg = 0;
                if (getParent() != null) {
                    if (getParent().getParent() != null) {
                        marg += ((RelativeLayout) getParent().getParent()).getPaddingTop();
                        marg += ((RelativeLayout) getParent().getParent()).getPaddingBottom();
                    }
                }

                int iw = imageWidth;
                int ih = imageHeight;

                width = height * iw / ih;
                height -= marg;
                setMeasuredDimension(width, height);
            }

        }
    }

以上根据宽高属性设置的判断,对实际宽高进行重新设置,先看下布局文件宽高设置:

  <me.maxwin.view.XListView
        xmlns:pla="http://schemas.android.com/apk/res-auto"
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fastScrollEnabled="true"
        android:scrollbars="vertical"
        pla:plaColumnNumber="3" />

那就是scaleToWidth = true;根据图片资源的宽高比计算出iamgeview的高:int heightC = width * ih / iw;图片居中显示。

     根据瀑布流显示列数和边界可以计算出显示图片的imageview的宽。

     上面还限制了imageview显示高的上限,如果超过的话,就来缩小imageview的宽,也就是宽不一定是match_parent。

4、自定义MultiColumnListView的实现原理

   创建瀑布流view时候首先初始化列数:

private void init(AttributeSet attrs) {
        getWindowVisibleDisplayFrame(mFrameRect);

        if (attrs == null) {
            mColumnNumber = (DEFAULT_COLUMN_NUMBER); // default column number is
                                                     // 2.
        } else {
            TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.PinterestLikeAdapterView);

            int landColNumber = a.getInteger(R.styleable.PinterestLikeAdapterView_plaLandscapeColumnNumber, 3);
            int defColNumber = a.getInteger(R.styleable.PinterestLikeAdapterView_plaColumnNumber, DEFAULT_COLUMN_NUMBER);

            if (mFrameRect.width() > mFrameRect.height() && landColNumber != -1) {
                mColumnNumber = (landColNumber);
            } else if (defColNumber != -1) {
                mColumnNumber = (defColNumber);
            } else {
                mColumnNumber = (DEFAULT_COLUMN_NUMBER);
            }

            mColumnPaddingLeft = a.getDimensionPixelSize(R.styleable.PinterestLikeAdapterView_plaColumnPaddingLeft, 0);
            mColumnPaddingRight = a.getDimensionPixelSize(R.styleable.PinterestLikeAdapterView_plaColumnPaddingRight, 0);
            a.recycle();
        }

        mColumns = new Column[getColumnNumber()];
        for (int i = 0; i < getColumnNumber(); ++i)
            mColumns[i] = new Column(i);

        mFixedColumn = new FixedColumn();
    }

可以看出列数没有设置的话,会有一个默认值:DEFAULT_COLUMN_NUMBER。

根据设置的列数,定义了一个列数组,在存储每一列的数据:mColumns = new Column[getColumnNumber()];

初始化每一列的宽度和横向位置:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        columnWidth = (getMeasuredWidth() - mListPadding.left - mListPadding.right - mColumnPaddingLeft - mColumnPaddingRight)
                / getColumnNumber();

        for (int index = 0; index < getColumnNumber(); ++index) {
            mColumns[index].mColumnWidth = columnWidth;
            mColumns[index].mColumnLeft = mListPadding.left + mColumnPaddingLeft + columnWidth * index;
        }

        mFixedColumn.mColumnLeft = mListPadding.left;
        mFixedColumn.mColumnWidth = getMeasuredWidth();
    }

接下来就是想每一列中添加数据显示了,大体原理就是:

1、判断是在在头部添加还是尾部添加,也就是上拉还是下拉。

2、通过计算对比每一列的高,在最短的一列添加上要添加的数据。

3、根据添加后的数据,刷新布局显示。

原理如此,过程比较复杂,如果耐心足够的话可以认真研究下。


这里只是介绍了大体的实现原理,至于详细的控件自定义还有图片加载逻辑,还是慢慢分析代码吧。可以到github下载最新的,csdn我也传了一份暂时最新的:http://download.csdn.net/detail/xiangxue336/7059861




 

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
package com.youxiachai.onexlistview; import me.maxwin.view.IXListViewLoadMore; import me.maxwin.view.IXListViewRefreshListener; import me.maxwin.view.IXScrollListener; import me.maxwin.view.XListViewFooter; import me.maxwin.view.XListViewHeader; import android.content.Context; import android.os.Handler; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.view.animation.DecelerateInterpolator; import android.widget.ListAdapter; import android.widget.RelativeLayout; import android.widget.Scroller; import android.widget.TextView; import com.huewu.pla.lib.MultiColumnListView; import com.huewu.pla.lib.internal.PLA_AbsListView; import com.huewu.pla.lib.internal.PLA_AbsListView.OnScrollListener; /** * @author youxiachai * @date 2013-5-3 */ public class XMultiColumnListView extends MultiColumnListView implements OnScrollListener { protected float mLastY = -1; // save event y protected Scroller mScroller; // used for scroll back protected OnScrollListener mScrollListener; // user's scroll listener // the interface to trigger refresh and load more. protected IXListViewLoadMore mLoadMore; protected IXListViewRefreshListener mOnRefresh; // -- header view protected XListViewHeader mHeaderView; // header view content, use it to calculate the Header's height. And hide it // when disable pull refresh. protected RelativeLayout mHeaderViewContent; protected TextView mHeaderTimeView; protected int mHeaderViewHeight; // header view's height protected boolean mEnablePullRefresh = true; protected boolean mPullRefreshing = false; // is refreashing. // -- footer view protected XListViewFooter mFooterView; protected boolean mEnablePullLoad; protected boolean mPullLoading; protected boolean mIsFooterReady = false; // total list items, used to detect is at the bottom of listview. protected int mTotalItemCount; // for mScroller, scroll back from header or footer. protected int mScrollBack; protected final static int SCROLLBACK_HEADER = 0; protected final static int SCROLLBACK_FOOTER = 1; protected final static int SCROLL_DURATION = 400; // scroll back duration protected final static int PULL_LOAD_MORE_DELTA = 50; // when pull up >= // 50px // at bottom, // trigger // load more. protected final static float OFFSET_RADIO = 1.8f; // support iOS like pull // feature. public XMultiColumnListView(Context context) { super(context); initWithContext(context); } public XMultiColumnListView(Context context, AttributeSet attrs) { super(context, attrs); initWithContext(context); } public XMultiColumnListView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initWithContext(context); } protected void initWithContext(Context context) { mScroller = new Scroller(context, new DecelerateInterpolator()); // XListView need the scroll event, and it will dispatch the event to // user's listener (as a proxy). super.setOnScrollListener(this); // init header view mHeaderView = new XListViewHeader(context); mHeaderViewContent = (RelativeLayout) mHeaderView .findViewById(R.id.xlistview_header_content); mHeaderTimeView = (TextView) mHeaderView .findViewById(R.id.xlistview_header_time); addHeaderView(mHeaderView); // init footer view mFooterView = new XListViewFooter(context); // init header height mHeaderView.getViewTreeObserver().addOnGlobalLayoutListener( new OnGlobalLayoutListener() { @Override public void onGlobalLayout() { mHeaderViewHeight = mHeaderViewContent.getHeight(); getViewTreeObserver() .removeGlobalOnLayoutListener(this); } }); // 默认关闭所有操作 disablePullLoad(); disablePullRefreash(); // setPullRefreshEnable(mEnablePullRefresh); // setPullLoadEnable(mEnablePullLoad); } public void updateHeaderHeight(float delta) { mHeaderView.setVisiableHeight((int) delta + mHeaderView.getVisiableHeight()); if (mEnablePullRefresh && !mPullRefreshing) { // 未处于刷新状态,更新箭头 if (mHeaderView.getVisiableHeight() > mHeaderViewHeight) { mHeaderView.setState(XListViewHeader.STATE_READY); } else { mHeaderView.setState(XListViewHeader.STATE_NORMAL); } } setSelection(0); // scroll to top each time } protected void invokeOnScrolling() { if (mScrollListener instanceof IXScrollListener) { IXScrollListener l = (IXScrollListener) mScrollListener; l.onXScrolling(this); } } protected void startLoadMore() { if (mEnablePullLoad && mFooterView.getBottomMargin() > PULL_LOAD_MORE_DELTA && !mPullLoading) { mPullLoading = true; mFooterView.setState(XListViewFooter.STATE_LOADING); if (mLoadMore != null) { mLoadMore.onLoadMore(); } } } protected void resetFooterHeight() { int bottomMargin = mFooterView.getBottomMargin(); if (bottomMargin > 0) { mScrollBack = SCROLLBACK_FOOTER; mScroller.startScroll(0, bottomMargin, 0, -bottomMargin, SCROLL_DURATION); invalidate(); } } protected void updateFooterHeight(float delta) { int height = mFooterView.getBottomMargin() + (int) delta; if (mEnablePullLoad && !mPullLoading) { if (height > PULL_LOAD_MORE_DELTA) { // height enough to invoke load // more. mFooterView.setState(XListViewFooter.STATE_READY); } else { mFooterView.setState(XListViewFooter.STATE_NORMAL); } } mFooterView.setBottomMargin(height); // setSelection(mTotalItemCount - 1); // scroll to bottom } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); // make sure XListViewFooter is the last footer view, and only add once. if (mIsFooterReady == false) { // if not inflate screen ,footerview not add if(getAdapter() != null){ if (getLastVisiblePosition() != (getAdapter().getCount() - 1)) { mIsFooterReady = true; addFooterView(mFooterView); } } } } /** * reset header view's height. */ public void resetHeaderHeight() { int height = mHeaderView.getVisiableHeight(); if (height == 0) // not visible. return; // refreshing and header isn't shown fully. do nothing. if (mPullRefreshing && height <= mHeaderViewHeight) { return; } int finalHeight = 0; // default: scroll back to dismiss header. // is refreshing, just scroll back to show all the header. if (mPullRefreshing && height > mHeaderViewHeight) { finalHeight = mHeaderViewHeight; } Log.d("xlistview", "resetHeaderHeight-->" + (finalHeight - height)); mScrollBack = SCROLLBACK_HEADER; mScroller.startScroll(0, height, 0, finalHeight - height, SCROLL_DURATION); // trigger computeScroll invalidate(); } /* * 神奇的bug.... */ @Override public void setAdapter(ListAdapter adapter) { super.setAdapter(adapter); //莫名其妙的bug.... //updateHeaderHeight(10); postDelayed(new Runnable() { @Override public void run() { // resetHeaderHeight(); mScroller.startScroll(0, 0, 0, 0, SCROLL_DURATION); // // trigger computeScroll invalidate(); } }, 100); } /** * enable or disable pull down refresh feature. * * @param enable */ public void setPullRefreshEnable(IXListViewRefreshListener refreshListener) { mEnablePullRefresh = true; mHeaderViewContent.setVisibility(View.VISIBLE); this.mOnRefresh = refreshListener; } public void disablePullRefreash() { mEnablePullRefresh = false; // disable, hide the content mHeaderViewContent.setVisibility(View.INVISIBLE); } /** * enable or disable pull up load more feature. * * @param enable */ public void setPullLoadEnable(IXListViewLoadMore loadMoreListener) { mEnablePullLoad = true; this.mLoadMore = loadMoreListener; mPullLoading = false; mFooterView.show(); mFooterView.setState(XListViewFooter.STATE_NORMAL); // both "pull up" and "click" will invoke load more. mFooterView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { startLoadMore(); } }); } public void disablePullLoad() { mEnablePullLoad = false; mFooterView.hide(); mFooterView.setOnClickListener(null); } /** * set last refresh time * * @param time */ public void setRefreshTime(String time) { mHeaderTimeView.setText(time); } /** * stop refresh, reset header view. */ public void stopRefresh(String time) { if (mPullRefreshing == true) { mPullRefreshing = false; mHeaderTimeView.setText(time); resetHeaderHeight(); } } /** * stop load more, reset footer view. */ public void stopLoadMore() { if (mPullLoading == true) { mPullLoading = false; mFooterView.setState(XListViewFooter.STATE_NORMAL); } } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { if (mScrollBack == SCROLLBACK_HEADER) { mHeaderView.setVisiableHeight(mScroller.getCurrY()); } else { mFooterView.setBottomMargin(mScroller.getCurrY()); } postInvalidate(); invokeOnScrolling(); } super.computeScroll(); } @Override public boolean onTouchEvent(MotionEvent ev) { if (mLastY == -1) { mLastY = ev.getRawY(); } switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mLastY = ev.getRawY(); break; case MotionEvent.ACTION_MOVE: final float deltaY = ev.getRawY() - mLastY; mLastY = ev.getRawY(); Log.d("xlistview", "getFirstVisiblePosition()-->" + getFirstVisiblePosition() + "getVisiableHeight()" + mHeaderView.getVisiableHeight() + "deltaY->" + deltaY); if (getFirstVisiblePosition() == 0 && (mHeaderView.getVisiableHeight() > 0 || deltaY > 0) && !mPullRefreshing) { // the first item is showing, header has shown or pull down. if(mEnablePullRefresh){ updateHeaderHeight(deltaY / OFFSET_RADIO); invokeOnScrolling(); } } else if (getLastVisiblePosition() == mTotalItemCount - 1 && (mFooterView.getBottomMargin() > 0 || deltaY < 0) && !mPullLoading) { // last item, already pulled up or want to pull up. if(mEnablePullLoad){ updateFooterHeight(-deltaY / OFFSET_RADIO); } } break; default: mLastY = -1; // reset if (getFirstVisiblePosition() == 0) { // invoke refresh startOnRefresh(); resetHeaderHeight(); } else if (getLastVisiblePosition() == mTotalItemCount - 1) { // invoke load more. startLoadMore(); resetFooterHeight(); } break; } return super.onTouchEvent(ev); } protected void startOnRefresh() { if (mEnablePullRefresh && mHeaderView.getVisiableHeight() > mHeaderViewHeight && !mPullRefreshing) { mPullRefreshing = true; mHeaderView.setState(XListViewHeader.STATE_REFRESHING); if (mOnRefresh != null) { mOnRefresh.onRefresh(); } } } @Override public void onScrollStateChanged(PLA_AbsListView view, int scrollState) { if (mScrollListener != null) { mScrollListener.onScrollStateChanged(view, scrollState); } } @Override public void onScroll(PLA_AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { // send to user's listener mTotalItemCount = totalItemCount; if (mScrollListener != null) { mScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); } } @Override public void setOnScrollListener(OnScrollListener l) { mScrollListener = l; } }
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值