Библиотека Header2ActionBar для Android
(демо для привлечения внимания)
Вы, наверное, уже видели похожее в приложениях от Google (Play Музыка, Google Пресса) и, возможно, каких-либо других. Для этих целей уже довольно давно существует библиотека от ManuelPeinado — FadingActionBar, которая прекрасно выполняет свою задачу, но к сожалению, имеет два «фатальных» недостатка.
Второй из них описан как известная проблема:
Known Issues
There is an important issue with the library and ListViews. More specifically, things don't work quite right when the activity is re-created due to a configuration change. So, unless you handle configuration changes yourself (or your activity is portrait/landscape only), I strongly suggest you stick to having your content in a ScrollView until a solution to this issue is found.
Стараясь исправить этот недостаток, я решил написать свою реализацию, тем самым устранив и оба недостатка :)
Библиотека состоит из трёх файлов:
FadingActionBarActivity.java
/**
* Created by AChep@xda <artemchep@gmail.com>
*/
public class FadingActionBarActivity extends Activity {
private static final String TAG = "FadingActionBarActivity";
private int mAlpha = 255;
private Drawable mDrawable;
private boolean isAlphaLocked;
public void setActionBarBackgroundDrawable(Drawable drawable) {
getActionBar().setBackgroundDrawable(drawable);
mDrawable = drawable;
if (mAlpha == 255) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
mAlpha = drawable.getAlpha();
} else {
setActionBarAlpha(mAlpha);
}
}
/**
* An {@link android.app.ActionBar} background drawable.
*
* @see #setActionBarBackgroundDrawable(android.graphics.drawable.Drawable)
* @see #setActionBarAlpha(int)
*/
public Drawable getActionBarBackgroundDrawable() {
return mDrawable;
}
/**
* Please use this method for global changes only!
* Otherwise, please, use {@link android.graphics.drawable.Drawable#setAlpha(int)}
* to {@link #getActionBarBackgroundDrawable()} directly.
*
* @param alpha a value from 0 to 255
* @see #getActionBarBackgroundDrawable()
* @see #getActionBarAlpha()
*/
public void setActionBarAlpha(int alpha) {
if (mDrawable == null) {
Log.w(TAG, "Set action bar background before setting alpha!");
return;
}
if (!isAlphaLocked) mDrawable.setAlpha(alpha);
mAlpha = alpha;
}
public int getActionBarAlpha() {
return mAlpha;
}
public void setActionBarAlphaLocked(boolean isLocked) {
isAlphaLocked = isLocked;
}
}
HeaderFragment .java
/**
* Little header fragment.
* <p>
* Created by AChep@xda <artemchep@gmail.com>
* </p>
*/
public class HeaderFragment extends Fragment {
private static final String TAG = "HeaderFragment";
private FrameLayout mRoot;
private View mContentOverlay;
private View mHeader;
private int mHeaderHeight;
private int mCurrentHeaderHeight;
private int mCurrentHeaderTranslateY;
private Space mFakeHeader;
private boolean mListViewEmpty;
private OnHeaderScrollChangeListener mOnHeaderScrollChangeListener;
public interface OnHeaderScrollChangeListener {
public void onHeaderScrollChanged(float progress, int height, int scroll);
}
public void setOnHeaderScrollChangeListener(OnHeaderScrollChangeListener listener) {
mOnHeaderScrollChangeListener = listener;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final Activity activity = getActivity();
mHeader = inflater.inflate(getHeaderResource(), container, false);
mHeaderHeight = mHeader.getLayoutParams().height;
mCurrentHeaderHeight = mHeaderHeight;
mCurrentHeaderTranslateY = 0;
onPrepareHeaderView(mHeader);
// Perform fake header view.
mFakeHeader = new Space(activity);
mFakeHeader.setLayoutParams(new ListView.LayoutParams(
0, mHeaderHeight));
View content = inflater.inflate(getContentResource(), container, false);
assert content != null;
if (content instanceof ListView) {
final ListView listView = (ListView) content;
mListViewEmpty = true;
listView.addHeaderView(mFakeHeader);
onPrepareContentListView(listView);
listView.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView absListView, int i) { /* unused */ }
@Override
public void onScroll(AbsListView absListView, int i, int i2, int i3) {
if (mListViewEmpty) { // poor poor listview :(
updateHeaderScroll(0);
} else {
final View child = absListView.getChildAt(0);
if (child == mFakeHeader) {
updateHeaderScroll(child.getTop());
} else {
updateHeaderScroll(-mHeaderHeight);
}
}
}
});
} else {
onPrepareContentView(content);
// Merge fake header view and content view
final LinearLayout ll = new LinearLayout(activity);
ll.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
ll.setOrientation(LinearLayout.VERTICAL);
ll.addView(mFakeHeader);
ll.addView(content);
final NotifyingScrollView scrollView = new NotifyingScrollView(activity);
scrollView.addView(ll);
scrollView.setOnScrollChangedListener(new NotifyingScrollView.OnScrollChangedListener() {
@Override
public void onScrollChanged(ScrollView who, int l, int t, int oldl, int oldt) {
updateHeaderScroll(-t);
}
});
content = scrollView;
}
mRoot = new FrameLayout(activity);
mRoot.addView(content, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
mRoot.addView(mHeader);
// Overlay view always shows at the top of content.
mContentOverlay = onCreateContentOverlayView();
if (mContentOverlay != null) {
mRoot.addView(mContentOverlay, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
}
// Initial notify
notifyOnHeaderScrollChangeListener(0, mHeaderHeight, 0);
return mRoot;
}
private void updateHeaderScroll(int scrollTo) {
scrollTo = scrollTo > 0 ? 0 : scrollTo < -mHeaderHeight ? -mHeaderHeight : scrollTo;
final boolean allowChangeHeight = isHeaderHeightFloating();
final int height = mHeaderHeight + scrollTo / 2;
final int transY = allowChangeHeight ? scrollTo / 2 : scrollTo;
if (height != mCurrentHeaderHeight && allowChangeHeight) {
final ViewGroup.LayoutParams lp = mHeader.getLayoutParams();
lp.height = height;
mHeader.setLayoutParams(lp);
mCurrentHeaderHeight = height;
}
if (transY != mCurrentHeaderTranslateY) {
mHeader.setTranslationY(transY);
mCurrentHeaderTranslateY = transY;
if (mContentOverlay != null) {
final ViewGroup.LayoutParams lp = mContentOverlay.getLayoutParams();
final int delta = mHeaderHeight + scrollTo;
lp.height = mRoot.getHeight() - delta;
mContentOverlay.setLayoutParams(lp);
mContentOverlay.setTranslationY(delta);
}
notifyOnHeaderScrollChangeListener((float) -scrollTo / mHeaderHeight,
mHeaderHeight, -scrollTo);
}
}
private void notifyOnHeaderScrollChangeListener(float progress, int height, int scroll) {
if (mOnHeaderScrollChangeListener != null) {
// Notify upper fragment to update ActionBar's alpha or whatever.
mOnHeaderScrollChangeListener.onHeaderScrollChanged(progress, height, scroll);
}
}
/**
* If true, header's height might be changed on scroll.
* <p>Note: It takes a lot of calculations to measure the header all the time.</p>
*/
public boolean isHeaderHeightFloating() {
return false;
}
/**
* Int reference to header's resource.
*
* @see #onPrepareHeaderView(android.view.View)
* @see #getContentResource()
*/
public int getHeaderResource() {
return 0;
}
/**
* This is the place for setting up the header.
*
* @param view inflated header view.
* @see #getHeaderResource()
*/
public void onPrepareHeaderView(View view) { /* for my child */ }
/**
* Int reference to content's resource.
* <p>
* <b>Attention</b>: Parent view must be {@link android.widget.ListView ListView}
* or something else which will work inside of {@link android.widget.ScrollView ScrollView}.
* Otherwise it <b>WON'T</b> work.
* </p>
*
* @see #getHeaderResource()
* @see #onPrepareContentListView(ListView)
*/
public int getContentResource() {
return 0;
}
/**
* Called if the content's parent is a {@link android.widget.ListView ListView}.
*
* @see #getContentResource()
* @see #setListViewAdapter(android.widget.ListView, android.widget.ListAdapter)
*/
public void onPrepareContentListView(ListView listView) { /* for my child */ }
public void setListViewAdapter(ListView listView, ListAdapter adapter) {
mListViewEmpty = adapter == null;
listView.removeHeaderView(mFakeHeader);
listView.addHeaderView(mFakeHeader);
listView.setAdapter(adapter);
}
/**
* Called if the content's parent is NOT a {@link android.widget.ListView ListView}.
*
* @see #getContentResource()
*/
public void onPrepareContentView(View view) { /* for my child */ }
public View onCreateContentOverlayView() {
return null;
}
}
NotifyingScrollView .java
/**
* @author Cyril Mottier with modifications from Manuel Peinado
*/
public class NotifyingScrollView extends ScrollView {
// Edge-effects don't mix well with the translucent action bar in Android 2.X
private boolean mDisableEdgeEffects = true;
/**
* @author Cyril Mottier
*/
public interface OnScrollChangedListener {
void onScrollChanged(ScrollView who, int l, int t, int oldl, int oldt);
}
private OnScrollChangedListener mOnScrollChangedListener;
public NotifyingScrollView(Context context) {
super(context);
}
public NotifyingScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public NotifyingScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (mOnScrollChangedListener != null) {
mOnScrollChangedListener.onScrollChanged(this, l, t, oldl, oldt);
}
}
public void setOnScrollChangedListener(OnScrollChangedListener listener) {
mOnScrollChangedListener = listener;
}
@Override
protected float getTopFadingEdgeStrength() {
// http://stackoverflow.com/a/6894270/244576
if (mDisableEdgeEffects && Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
return 0.0f;
}
return super.getTopFadingEdgeStrength();
}
@Override
protected float getBottomFadingEdgeStrength() {
// http://stackoverflow.com/a/6894270/244576
if (mDisableEdgeEffects && Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
return 0.0f;
}
return super.getBottomFadingEdgeStrength();
}
}
и лежит на GitHub'е как проект библиотеки созданной в Android Studio.
Использование
HeaderFragment и FadingActionBarActivity наследуются от нативных собратьев, так что пока Android < 4.0 не поддерживается из коробки.
Наше приложение будет подобием демо на скриншоте сверху. Итак, пример Activity:
public class MainActivity extends FadingActionBarActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Инициализация фона ActionBar'a
setActionBarBackgroundDrawable(getResources().getDrawable(R.drawable.actionbar_bg));
FragmentManager fragmentManager = getFragmentManager();
fragmentManager.beginTransaction()
.replace(R.id.container, new TestHeaderFragment()
).commit();
}
}
public class TestHeaderFragment extends HeaderFragment {
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// Меняем прозрачность ActionBar'a во время скроллинга
setOnHeaderScrollChangeListener(new OnHeaderScrollChangeListener() {
@Override
public void onHeaderScrollChanged(float progress, int height, int scroll) {
height -= getActivity().getActionBar().getHeight();
progress = (float) scroll / height;
if (progress > 1f) progress = 1f;
((FadingActionBarActivity) getActivity()).setActionBarAlpha((int) (255 * progress));
}
});
}
@Override
public int getHeaderResource() {
return R.layout.header;
}
@Override
public void onPrepareHeaderView(View view) {
super.onPrepareHeaderView(view);
// Заполняем view контентом
}
@Override
public int getContentResource() {
return R.layout.content;
}
@Override
public void onPrepareContentListView(ListView listView) {
super.onPrepareContentListView(listView);
// Заполняем view контентом
setListViewAdapter(listView, new ArrayAdapter<>(getActivity(), android.R.layout.simple_list_item_1, android.R.id.title, new String[]{"Android", "Android", "Android", "Android", "Android", "Android", "Android", "Android", "Android", "Android", "Android"}));
}
Так-же в стиль Activity необходимо добавить флаг:
<item name="android:windowActionBarOverlay">true</item>
, что бы разрешить контенту находиться под ActionBar'ом
Я заранее закрываю свое лицо руками и прошу прощения за свой код и английский. :(