/*
* Copyright (c) 2015 [1076559197@qq.com | tchen0707@gmail.com]
*
* Licensed under the Apache License, Version 2.0 (the "Licenseā);
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.aisen.android.ui.widget.pla;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.View;
import org.aisen.android.R;
/**
* @author huewu.ynag
* @date 2012-11-06
*/
public class PLAMultiColumnListView extends PLAListView {
@SuppressWarnings("unused")
private static final String TAG = "MultiColumnListView";
private static final int DEFAULT_COLUMN_NUMBER = 2;
private int mColumnNumber = 2;
private Column[] mColumns = null;
private Column mFixedColumn = null; //column for footers & headers.
private ParcelableSparseIntArray mItems = new ParcelableSparseIntArray();
private int mColumnPaddingLeft = 0;
private int mColumnPaddingRight = 0;
public PLAMultiColumnListView(Context context) {
super(context);
init(null);
}
public PLAMultiColumnListView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
public PLAMultiColumnListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(attrs);
}
private Rect mFrameRect = new Rect();
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.PLAMultiColumnListView);
int landColNumber = a.getInteger(R.styleable.PLAMultiColumnListView_plaLandscapeColumnNumber, -1);
int defColNumber = a.getInteger(R.styleable.PLAMultiColumnListView_plaColumnNumber, -1);
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.PLAMultiColumnListView_plaColumnPaddingLeft, 0);
mColumnPaddingRight = a.getDimensionPixelSize(R.styleable.PLAMultiColumnListView_plaColumnPaddingRight, 0);
a.recycle();
}
mColumns = new Column[mColumnNumber];
for( int i = 0; i < mColumnNumber; ++i )
mColumns[i] = new Column(i);
mFixedColumn = new FixedColumn();
}
public void setColumnPaddingLeft(int columnPaddingLeft) {
this.mColumnPaddingLeft = columnPaddingLeft;
}
public void setColumnPaddingRight(int columnPaddingRight) {
this.mColumnPaddingRight = columnPaddingRight;
}
///////////////////////////////////////////////////////////////////////
//Override Methods...
///////////////////////////////////////////////////////////////////////
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
//TODO the adapter status may be changed. what should i do here...
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = (getMeasuredWidth() - mListPadding.left - mListPadding.right - mColumnPaddingLeft - mColumnPaddingRight) / mColumnNumber;
for( int index = 0; index < mColumnNumber; ++ index ){
mColumns[index].mColumnWidth = width;
mColumns[index].mColumnLeft = mListPadding.left + mColumnPaddingLeft + width * index;
}
mFixedColumn.mColumnLeft = mListPadding.left;
mFixedColumn.mColumnWidth = getMeasuredWidth();
}
@Override
protected void onMeasureChild(View child, int position, int widthMeasureSpec, int heightMeasureSpec) {
if(isFixedView(child))
child.measure(widthMeasureSpec, heightMeasureSpec);
else
child.measure(MeasureSpec.EXACTLY | getColumnWidth(position), heightMeasureSpec);
}
@Override
protected int modifyFlingInitialVelocity(int initialVelocity) {
return initialVelocity;
}
@Override
protected void onItemAddedToList(int position, boolean flow ) {
super.onItemAddedToList(position, flow);
if( isHeaderOrFooterPosition(position) == false){
Column col = getNextColumn( flow, position );
mItems.append(position, col.getIndex());
}
}
@Override
protected void onLayoutSync(int syncPos) {
for( Column c : mColumns ){
c.save();
}
}
@Override
protected void onLayoutSyncFinished(int syncPos) {
for( Column c : mColumns ){
c.clear();
}
}
@Override
protected void onAdjustChildViews(boolean down) {
int firstItem = getFirstVisiblePosition();
if(!down && firstItem == 0){
final int firstColumnTop = mColumns[0].getTop();
for( Column c : mColumns ){
final int top = c.getTop();
//align all column's top to 0's column.
c.offsetTopAndBottom( firstColumnTop - top );
}
}
super.onAdjustChildViews(down);
}
@Override
protected int getFillChildBottom() {
//return smallest bottom value.
//in order to determine fill down or not... (calculate below space)
int result = Integer.MAX_VALUE;
for(Column c : mColumns){
int bottom = c.getBottom();
result = result > bottom ? bottom : result;
}
return result;
}
@Override
protected int getFillChildTop() {
//find largest column.
int result = Integer.MIN_VALUE;
for(Column c : mColumns){
int top = c.getTop();
result = Math.max(result, top);
}
return result;
}
@Override
protected int getScrollChildBottom() {
//return largest bottom value.
//for checking scrolling region...
int result = Integer.MIN_VALUE;
for(Column c : mColumns){
int bottom = c.getBottom();
result = result < bottom ? bottom : result;
}
return result;
}
@Override
protected int getScrollChildTop() {
//find largest column.
int result = Integer.MAX_VALUE;
for(Column c : mColumns){
int top = c.getTop();
result = result > top ? top : result;
}
return result;
}
@Override
protected int getItemLeft(int pos) {
if( isHeaderOrFooterPosition(pos) )
return mFixedColumn.getColumnLeft();
return getColumnLeft(pos);
}
@Override
protected int getItemTop( int pos ){
if( isHeaderOrFooterPosition(pos) )
return mFixedColumn.getBottom(); //footer view should be placed below the last column.
int colIndex = mItems.get(pos, -1);
if(colIndex == -1)
return getFillChildBottom();
return mColumns[colIndex].getBottom();
}
@Override
protected int getItemBottom( int pos ){
if( isHeaderOrFooterPosition(pos) )
return mFixedColumn.getTop(); //header view should be place above the first column item.
int colIndex = mItems.get(pos, -1);
if(colIndex == -1)
return getFillChildTop();
return mColumns[colIndex].getTop();
}
//////////////////////////////////////////////////////////////////////////////
//Private Methods...
//////////////////////////////////////////////////////////////////////////////
//flow If flow is true, align top edge to y. If false, align bottom edge to y.
private Column getNextColumn(boolean flow, int position) {
//we already have this item...
int colIndex = mItems.get(position, -1);
if( colIndex != -1 ){
return mColumns[colIndex];
}
//adjust position (exclude headers...)
position = Math.max(0, position - getHeaderViewsCount());
final int lastVisiblePos = Math.max( 0, position );
if( lastVisiblePos < mColumnNumber )
return mColumns[lastVisiblePos];
if( flow ){
//find column which has the smallest bottom value.
return gettBottomColumn();
}else{
//find column which has the smallest top value.
return getTopColumn();
}
}
private boolean isHeaderOrFooterPosition( int pos ){
int type = mAdapter.getItemViewType(pos);
return type == ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
}
private Column getTopColumn() {
Column result = mColumns[0];
for( Column c : mColumns ){
result = result.getTop() > c.getTop() ? c : result;
}
return result;
}
private Column gettBottomColumn() {
Column result = mColumns[0];
for( Column c : mColumns ){
result = result.getBottom() > c.getBottom() ? c : result;
}
return result;
}
private int getColumnLeft(int pos) {
int colIndex = mItems.get(pos, -1);
if( colIndex == -1 )
return 0;
return mColumns[colIndex].getColumnLeft();
}
private int getColumnWidth(int pos) {
int colIndex = mItems.get(pos, -1 );
if( colIndex == -1 )
return 0;
return mColumns[colIndex].getColumnWidth();
}
///////////////////////////////////////////////////////////////
//Inner Class.
///////////////////////////////////////////////////////////////
private class Column {
private int mIndex;
private int mColumnWidth;
private int mColumnLeft;
private int mSynchedTop = 0;
private int mSynchedBottom = 0;
//TODO is it ok to use item position info to identify item??
public Column(int index) {
mIndex = index;
}
public int getColumnLeft() {
return mColumnLeft;
}
public int getColumnWidth() {
return mColumnWidth;
}
public int getIndex() {
return mIndex;
}
public int getBottom() {
//find biggest value.
int bottom = Integer.MIN_VALUE;
int childCount = getChildCount();
for( int index = 0; index < childCount; ++index ){
View v = getChildAt(index);
if(v.getLeft() != mColumnLeft && isFixedView(v) == false )
continue;
bottom = bottom < v.getBottom() ? v.getBottom() : bottom;
}
if( bottom == Integer.MIN_VALUE )
return mSynchedBottom; //no child for this column..
return bottom;
}
public void offsetTopAndBottom(int offset) {
if( offset == 0 )
return;
//find biggest value.
int childCount = getChildCount();
for( int index = 0; index < childCount; ++index ){
View v = getChildAt(index);
if(v.getLeft() != mColumnLeft && isFixedView(v) == false )
continue;
v.offsetTopAndBottom(offset);
}
}
public int getTop() {
//find smallest value.
int top = Integer.MAX_VALUE;
int childCount = getChildCount();
for( int index = 0; index < childCount; ++index ){
View v = getChildAt(index);
if(v.getLeft() != mColumnLeft && !isFixedView(v))
continue;
top = Math.min(top, v.getTop());
}
if( top == Integer.MAX_VALUE )
return mSynchedTop; //no child for this column. just return saved sync top..
return top;
}
public void save() {
mSynchedTop = 0;
mSynchedBottom = getTop();
}
public void clear() {
mSynchedTop = 0;
mSynchedBottom = 0;
}
}//end of inner class Column
private class FixedColumn extends Column {
public FixedColumn() {
super(Integer.MAX_VALUE);
}
@Override
public int getBottom() {
return getScrollChildBottom();
}
@Override
public int getTop() {
return getScrollChildTop();
}
}
private boolean loadingMoreComplete = true;
public void onLoadMoreComplete() {
loadingMoreComplete = true;
}
public interface OnLoadMoreListener {
/**
* Method to be called when scroll to buttom is requested
*/
void onLoadMore();
}
public OnScrollListener scroller = new OnScrollListener() {
private int visibleLastIndex = 0;
private static final int OFFSET = 2;
@Override
public void onScrollStateChanged(PLAAbsListView view, int scrollState) {
int lastIndex = getAdapter().getCount() - OFFSET;
if (scrollState == OnScrollListener.SCROLL_STATE_IDLE
&& visibleLastIndex == lastIndex && loadingMoreComplete) {
loadMoreListener.onLoadMore();
loadingMoreComplete = false;
}
}
@Override
public void onScroll(PLAAbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
visibleLastIndex = firstVisibleItem + visibleItemCount - OFFSET;
}
};
OnLoadMoreListener loadMoreListener;
public void setOnLoadMoreListener(OnLoadMoreListener listener) {
if (listener != null) {
this.loadMoreListener = listener;
this.setOnScrollListener(scroller);
}
}//end of class
@Override
public Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putParcelable("instanceState", super.onSaveInstanceState());
bundle.putParcelable("items", mItems);
return bundle;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
mItems = bundle.getParcelable("items");
state = bundle.getParcelable("instanceState");
}
super.onRestoreInstanceState(state);
}
}//end of class