package oak.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.ScrollView;
import oak.R;
/**
* Created by sean.kenkeremath on 7/29/14.
*/
public class ResizableHeaderScrollView extends FrameLayout {
private final static int SPACE_ID = 1001;
private final static int SCROLLVIEW_ID = 1002;
private final static int NOT_SPECIFIED = -1;
private SyncedScrollView scrollView;
private View headerFrame;
private View header;
private boolean headerCentered;
private boolean headerCollapsed;
private int minHeaderHeight;
private int maxHeaderHeight;
private int headerFlipScrollThreshold; //how much scrolling until header switches
private HeaderChangeListener headerListener;
public ResizableHeaderScrollView(Context context, int minHeaderHeight, int maxHeaderHeight,
int headerResId, int scrollViewLayoutId,
int headerFlipScrollThreshold, boolean headerCentered){
super(context);
this.headerCentered = headerCentered;
init(minHeaderHeight, maxHeaderHeight, headerResId, scrollViewLayoutId,
headerFlipScrollThreshold, NOT_SPECIFIED);
}
public ResizableHeaderScrollView(Context context, AttributeSet attr){
super(context, attr);
parseAttributes(attr);
}
public ResizableHeaderScrollView(Context context, AttributeSet attr, int defInStyles){
super(context, attr, defInStyles);
parseAttributes(attr);
}
private void parseAttributes(AttributeSet attr){
if (attr!=null){
TypedArray a = getContext().getTheme().obtainStyledAttributes(attr,R.styleable.ResizableHeaderScrollView,0,0);
if (a!=null){
headerCentered = a.getBoolean(R.styleable.ResizableHeaderScrollView_headerCentered, false);
minHeaderHeight = a.getDimensionPixelSize(R.styleable.ResizableHeaderScrollView_minHeaderHeight,NOT_SPECIFIED);
maxHeaderHeight = a.getDimensionPixelSize(R.styleable.ResizableHeaderScrollView_maxHeaderHeight,NOT_SPECIFIED);
headerFlipScrollThreshold = a.getDimensionPixelSize(R.styleable.ResizableHeaderScrollView_headerChangeThreshold,NOT_SPECIFIED);
int headerBackground = a.getResourceId(R.styleable.ResizableHeaderScrollView_resizableHeaderBackground, NOT_SPECIFIED);
int scrollViewRes = a.getResourceId(R.styleable.ResizableHeaderScrollView_contentLayout, NOT_SPECIFIED);
int headerRes = a.getResourceId(R.styleable.ResizableHeaderScrollView_headerLayout, NOT_SPECIFIED);
init(minHeaderHeight,maxHeaderHeight,headerRes,scrollViewRes,headerFlipScrollThreshold, headerBackground);
a.recycle();
return;
}
}
init(NOT_SPECIFIED,NOT_SPECIFIED,NOT_SPECIFIED,NOT_SPECIFIED,NOT_SPECIFIED,NOT_SPECIFIED);
}
private int getContentScrolled(){
if (scrollView.getScrollY()<0){
/**
* Negative scroll produces white space above header due to the logic in adjustChildren()
*/
return 0;
}
return scrollView.getScrollY();
}
public int getHeaderDelta(){
return maxHeaderHeight - minHeaderHeight;
}
private void init(int minHeaderHeight, int maxHeaderHeight, int headerRes,
int scrollViewLayoutId, int headerFlipThreshold, int headerBackground){
if (minHeaderHeight >maxHeaderHeight){
minHeaderHeight = maxHeaderHeight;
} else if (maxHeaderHeight < minHeaderHeight){
minHeaderHeight = maxHeaderHeight;
}
this.minHeaderHeight = minHeaderHeight;
this.maxHeaderHeight = maxHeaderHeight;
this.headerFlipScrollThreshold = headerFlipThreshold;
/**
* Header must be created first because the dimensions for spacing under the ScrollView will
* be based on the measured height of the header if minHeaderHeight is NOT_SPECIFIED
*/
createHeader(headerRes,headerBackground);
createScrollView(scrollViewLayoutId);
if (headerBackground!=NOT_SPECIFIED){
setHeaderBackground(headerBackground);
}
scrollView.setOnScrollListener(new OnScrollListener() {
@Override
public void onScrollChanged(int x, int y, int oldx, int oldy) {
if (!headerChangeEnabled()){
return;
}
/**
* Check how much the ScrollView has been scrolled and changed header if necessary
*/
if (getContentScrolled() > headerFlipScrollThreshold) {
if (headerListener!=null && !headerCollapsed){
headerListener.collapse(header);
headerCollapsed = true;
}
} else if (getContentScrolled() < headerFlipScrollThreshold) {
if (headerListener!=null && headerCollapsed) {
headerListener.expand(header);
headerCollapsed = false;
}
}
adjustChildren();
}
});
requestLayout();
}
public void setHeaderChangeListener(HeaderChangeListener listener){
this.headerListener= listener;
}
private void createScrollView(int scrollViewLayoutId){
/**
* This method creates a RelativeLayout containing a SyncedScrollView and a blank View.
* The blank view is necessary so the scrollview will cover the correct amount of space when
* the header has shrunken to its minimum size. The provided scrollViewLayoutId will be
* inflated into the SyncedScrollView.
*/
RelativeLayout layout = new RelativeLayout(getContext());
FrameLayout.LayoutParams relLayoutParams = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
layout.setLayoutParams(relLayoutParams);
this.addView(layout);
View space = new View(getContext());
//noinspection ResourceType
space.setId(SPACE_ID);
RelativeLayout.LayoutParams spaceParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT,
minHeaderHeight != NOT_SPECIFIED ? minHeaderHeight : 0);
spaceParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
space.setLayoutParams(spaceParams);
layout.addView(space);
scrollView = new SyncedScrollView(getContext(), this);
//noinspection ResourceType
scrollView.setId(SCROLLVIEW_ID);
RelativeLayout.LayoutParams scrollViewParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT,
RelativeLayout.LayoutParams.MATCH_PARENT);
scrollViewParams.addRule(RelativeLayout.ABOVE,space.getId());
scrollView.setLayoutParams(scrollViewParams);
LayoutInflater.from(getContext()).inflate(scrollViewLayoutId, scrollView);
layout.addView(scrollView);
}
private void createHeader(int headerResId, int headerBackgroundId){
/**
* This frame will be the parent layout of the entire header. This frame will move with
* scrolling while its children will move the opposite direction to give the appearance
* that the header is shrinking.
*/
LinearLayout frame = new LinearLayout(getContext());
if (headerBackgroundId != NOT_SPECIFIED){
frame.setBackgroundResource(headerBackgroundId);
}
/**
* The provided header resource is inflated into this container. The container will be
* a child of the frame created above and will move opposite to the frame to remain in place.
* If headerCentered is true, the container will move -1 for every 2 its parent moves during
* scrolling so it will stay centered.
*/
FrameLayout container = new FrameLayout(getContext());
FrameLayout.LayoutParams container_params = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT);
container.setLayoutParams(container_params);
LayoutInflater.from(getContext()).inflate(headerResId, container);
frame.addView(container);
headerFrame = frame;
header = container;
this.addView(headerFrame);
/**
* The height of the header layout is measured and will be used as default values if
* maxHeaderHeight or minHeaderHeight are NOT_SPECIFIED.
*/
headerFrame.measure(MeasureSpec.UNSPECIFIED,MeasureSpec.UNSPECIFIED);
int measuredHeight = headerFrame.getMeasuredHeight();
if (minHeaderHeight == NOT_SPECIFIED || minHeaderHeight < measuredHeight){
minHeaderHeight = measuredHeight;
}
if (maxHeaderHeight == NOT_SPECIFIED || maxHeaderHeight < measuredHeight){
maxHeaderHeight = measuredHeight;
}
if (minHeaderHeight >maxHeaderHeight){
minHeaderHeight = maxHeaderHeight;
} else if (maxHeaderHeight < minHeaderHeight){
minHeaderHeight = maxHeaderHeight;
}
/**
* The height for the header is set after the final maxHeaderHeight is determined.
*/
FrameLayout.LayoutParams frame_params = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
maxHeaderHeight);
frame.setLayoutParams(frame_params);
if (this.headerFlipScrollThreshold == NOT_SPECIFIED) {
this.headerFlipScrollThreshold = getHeaderDelta() > 0 ? getHeaderDelta() / 2 : NOT_SPECIFIED;
}
this.headerCollapsed = false;
}
public void setHeaderBackground(int headerBackgroundRes){
headerFrame.setBackgroundResource(headerBackgroundRes);
}
private boolean headerChangeEnabled(){
if (getHeaderDelta() <= 0){
return false;
}
return true;
}
// offset header frame, header, and scrollview based on scroll amount
private void adjustChildren(){
/**
* This method offsets the frame, header, and scrollview based on the current amount of
* scroll. Since there are gaps between OnScroll callbacks, the current position of each
* view is retrieved and the delta between where it should be and where it is is used to
* position them.
*/
if (getContentScrolled() < getHeaderDelta()) {
int headerOffset = 0 - getContentScrolled();
int headerCurrent = headerFrame.getTop();
headerFrame.offsetTopAndBottom(headerOffset - headerCurrent);
//cancel offset of frame
header.offsetTopAndBottom(-(headerOffset - headerCurrent));
if (headerCentered) {
//offset an additional amount to keep the layout centered within the frame
int layoutOffset = getContentScrolled() / 2;
int layoutCurrent = header.getTop();
header.offsetTopAndBottom(layoutOffset - layoutCurrent);
}
//offset scrollview based on height of header
int scrollViewOffset = maxHeaderHeight - getContentScrolled();
int scrollViewCurrent = scrollView.getTop();
scrollView.offsetTopAndBottom(scrollViewOffset - scrollViewCurrent);
} else {
int headerOffset = 0 - getHeaderDelta();
int headerCurrent = headerFrame.getTop();
headerFrame.offsetTopAndBottom(headerOffset - headerCurrent);
//cancel offset of frame
header.offsetTopAndBottom(-(headerOffset - headerCurrent));
if (headerCentered) {
//offset an additional amount to keep the layout centered within the frame
int layoutOffset = getHeaderDelta() / 2;
int layoutCurrent = header.getTop();
header.offsetTopAndBottom(layoutOffset - layoutCurrent);
}
//offset scrollview based on height of header
int scrollViewOffset = maxHeaderHeight - getHeaderDelta();
int scrollViewCurrent = scrollView.getTop();
scrollView.offsetTopAndBottom(scrollViewOffset - scrollViewCurrent);
}
}
public ScrollView getScrollView(){
return scrollView;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
adjustChildren();
}
private class SyncedScrollView extends ScrollView {
private boolean scrolling;
private float scrollAmount;
private ResizableHeaderScrollView parent;
public OnScrollListener listener;
public SyncedScrollView(Context context, ResizableHeaderScrollView parent) {
super(context);
this.parent = parent;
}
public SyncedScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SyncedScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public boolean onTouchEvent(MotionEvent e){
if (scrolling){
e.offsetLocation(0,scrollAmount);
}
switch (e.getAction()){
case MotionEvent.ACTION_MOVE:
scrolling = true;
break;
case MotionEvent.ACTION_DOWN:
scrolling = true;
break;
case MotionEvent.ACTION_UP:
scrolling = false;
scrollAmount = 0f;
break;
case MotionEvent.ACTION_CANCEL:
scrolling = false;
scrollAmount = 0f;
break;
}
return super.onTouchEvent(e);
}
public void setOnScrollListener(OnScrollListener listener){
this.listener = listener;
}
@Override
protected void onScrollChanged(int x, int y, int oldx, int oldy){
if (listener!=null){
listener.onScrollChanged(x,y,oldx,oldy);
}
if (scrolling && getScrollY() < parent.getHeaderDelta()){
scrollAmount += (oldy-y);
}
super.onScrollChanged(x,y,oldx,oldy);
}
}
public interface OnScrollListener{
void onScrollChanged(int x, int y, int oldx, int oldy);
}
public interface HeaderChangeListener{
void collapse(View header);
void expand(View header);
}
}