I have my main view with 3 Buttons in a vertical LinearLayout on top right of the screen. Each button shows some Fragment that is hosted inside a DrawerLayout. Like this:
When I click any button, I want the DrawerLayout to be shown from the right and this button to be translated alongside it. Like this:
I have managed to move the Button with the drawer but my problem is that the drawers's shadow affects the button as well. I want it to be so bright as the drawer's content (same hight) but I also want the other buttons to remain behind the drawer.
This is my activity_main.xml file:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<android.support.v4.widget.DrawerLayout
android:id="#+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="#layout/content_main"/>
<LinearLayout ... >
<ImageButton ... />
<ImageButton ... />
<ImageButton ... />
</LinearLayout>
<FrameLayout
android:id="#+id/fragment_secondary_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="right"
tools:context="..."/>
</android.support.v4.widget.DrawerLayout>
</android.support.design.widget.CoordinatorLayout>
With the buttons inside DrawerLayout, the drawer moves above them but the one that's being translated gets dark.
On the other hand if I put the buttons outside DrawerLayout, the button that moves looks all right but the drawer is below the other buttons. Like:
Is there any way to avoid DrawerLayout's shadow to affect a particular view? Or perhaps to cancel it?
DrawerLayout applies that shadow - the scrim - to its child Views in its drawChild() method. It determines which children to apply it to by checking that a given child is not a drawer View. It ultimately does this by checking the gravity of the child's LayoutParams.
Knowing this, we can create a custom DrawerLayout subclass to override drawChild(), and temporarily change the gravity on the child that we don't want the scrim applied to. Since no layout is happening during the draw, this won't affect the actual placement of the View.
public class CustomDrawerLayout extends DrawerLayout {
private int noScrimId;
public CustomDrawerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// Get the ID for the no-scrim View.
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomDrawerLayout);
noScrimId = a.getResourceId(R.styleable.CustomDrawerLayout_noScrimView, View.NO_ID);
a.recycle();
}
#Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
// Is this the child we want?
final boolean isNoScrimView = child.getId() == noScrimId;
// If yes, temporarily tag it as a drawer.
if (isNoScrimView) {
((LayoutParams) child.getLayoutParams()).gravity = Gravity.LEFT;
}
// Let the super class do the draw, and save the return.
final boolean res = super.drawChild(canvas, child, drawingTime);
// Reset the gravity if we changed it.
if (isNoScrimView) {
((LayoutParams) child.getLayoutParams()).gravity = Gravity.NO_GRAVITY;
}
return res;
}
}
This example uses a custom attribute so that the "no-scrim" View can be specified in the layout. If you don't want to use that attribute, you can remove the TypedArray processing from the constructor, and just hardcode an ID. If you do want to use the custom attribute, you need to define it in your project, which you can do by sticking the following file in the values/ folder, or adding to the one that might already be there.
attrs.xml
<resources>
<declare-styleable name="CustomDrawerLayout">
<attr name="noScrimView" format="reference" />
</declare-styleable>
</resources>
CustomDrawerLayout is a drop-in replacement, and you can use it in your layouts just like you would the regular class. For example, if your ImageButtons' LinearLayout has the ID button_menu:
<com.mycompany.myapp.CustomDrawerLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:noScrimView="#+id/button_menu">
Please note that if you should happen to set a drawer View as the noScrimView, you're gonna have a bad time. Also, the noScrimView must be a direct child of the DrawerLayout, as the scrim effect is applied only to those.
For simplicity's sake, I've omitted any checks from the sample, but if you wish to include some, doing them in the onMeasure() method would be in line with how DrawerLayout handles its own checks.
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final View v = findViewById(noScrimId);
if (v != null) {
if (!(v.getLayoutParams() instanceof LayoutParams)) {
throw new IllegalStateException("noScrimView must be a child of DrawerLayout");
}
if (((LayoutParams) v.getLayoutParams()).gravity != Gravity.NO_GRAVITY) {
throw new IllegalStateException("noScrimView cannot be a drawer View");
}
}
else {
if (noScrimId != View.NO_ID) {
Log.d(getClass().getSimpleName(), "noScrimView not found");
}
}
}
Related
I want to implement custom ViewGroup in my case derived from FrameLayout but I want all child views added from xml to be added not directly into this view but in FrameLayout contained in this custom ViewGroup.
Let me show example to make it clear.
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="#+id/frame_layout_child_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<FrameLayout
android:id="#+id/frame_layout_top"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</merge>
And I want to redirect adding all child view to FrameLayout with id frame_layout_child_container.
So of course I overrode methods addView() like this
#Override
public void addView(View child) {
this.mFrameLayoutChildViewsContainer.addView(child);
}
But for sure this doesn't work because for this time mFrameLayoutChildViewsContainer is not added to the root custom view.
My idea is always keep some view on on the top in this container frame_layout_top and all child views added into custom component should go to frame_layout_child_container
Example of using custom view
<CustomFrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"/>
</CustomFrameLayout>
So in this case TextView should be added to the frame_layout_child_container
Is it possible to delegate adding all views into child ViewGroup like I described.
I have other ideas like using bringToFront() method every time view is added to keep them in correct z-axis order or for example when view is added, save it to array and than after inflating custom view add all views to this child FrameLayout
Suggest what to do in this case in order not to hit performance with reinflating all layout every time new view is added, if it is possible to implement in other way.
Views inflated from a layout - like your example TextView - are not added to their parent ViewGroup with addView(View child), which is why overriding just that method didn't work for you. You want to override addView(View child, int index, ViewGroup.LayoutParams params), which all of the other addView() overloads end up calling.
In that method, check if the child being added is one of your two special FrameLayouts. If it is, let the super class handle the add. Otherwise, add the child to your container FrameLayout.
public class CustomFrameLayout extends FrameLayout {
private final FrameLayout topLayout;
private final FrameLayout containerLayout;
...
public CustomFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.custom, this, true);
topLayout = (FrameLayout) findViewById(R.id.frame_layout_top);
containerLayout = (FrameLayout) findViewById(R.id.frame_layout_child_container);
}
#Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
final int id = child.getId();
if (id == R.id.frame_layout_top || id == R.id.frame_layout_child_container) {
super.addView(child, index, params);
}
else {
containerLayout.addView(child, index, params);
}
}
}
I try to set up a MenuItem with an actionView as described here.
In my case the ActionView Widget is a SeekBar.
The problem is, that when the ActionView is shown on icon click, its width is way smaller than expected as you can see in the following screenshots:
The docs say:
If the widget is on the app bar, the app should display the widget as
an icon. If the widget is in the overflow menu, the app should display
the widget as a menu item. When the user interacts with the action
view, it expands to fill the app bar.
In my case, nothing gets "expanded". Instead of taking up the whole width of the ActionBar, its tiny.
To fix it I tried using another View like an EditText, but got similar Results. (The EditText is small on start, but expands on writing in it).
It seems to work with a android.support.v7.widget.SearchView as used in the docs.
I also tried it with Theme.AppCompat.Light.DarkActionBar or Theme.AppCompat.Light.NoActionBar providing a Toolbar in the layout without any noticeable difference.
Here is the code I used:
MainActivity
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/* set this on other layout with toolbar in it
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
*/
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.camera_menu, menu);
return true;
}
}
app_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="#+id/action_item"
android:title="Action"
android:icon="#drawable/ic_icon_in_black_24dp"
app:showAsAction="ifRoom|collapseActionView"
app:actionViewClass="android.widget.SeekBar"
/>
</menu>
I also tried app:actionViewLayout=#layout/seekbar_layout instead of actionViewClass
where seekbarLayout was something like this
<?xml version="1.0" encoding="utf-8"?>
<SeekBar
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/action_seekbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
Where I also tried different versions of ViewGroups around it like Linear, Relative or FrameLayouts, or tried to set a fixed width of my SeekBar without any success.
What Am I missing or doing wrong here?
Adding Seekbar as the menu item, will take up the size of default menu item.
If you need Seekbar to extend across the appbar, then you can create your own Toolbar and add SeekBar like this.
<youtLayoutRoot>
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="#style/AppTheme.PopupOverlay"
app:titleTextColor="#color/white" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1">
<SeekBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="100"
android:progress="10"/>
</LinearLayout>
</android.support.v7.widget.Toolbar>
<!-- your content layout here -->
</youtLayoutRoot>
Make the theme of your activity as Theme.AppCompat.Light.NoActionBar and run the application. You should be able to see the seekbar spanning across the toolbar.
In your activity just inflate the menu and it will take ut its size.
It works for me and hope that seekbar with menu item will also work for you.
toolbar.inflateMenu(R.menu.menu_delete)
toolbar.setNavigationIcon(R.drawable.vector_back)
toolbar.setNavigationOnClickListener {
onBackPressed()
}
toolbar.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.delete -> {
deleteCall()
true
}
else -> {
false
}
}
}
The "magic" takes place in Toolbar.expandItemActionView. This method overrides the layout parameters of the expanded ActionView and sets the width to wrap_content. Now to one possible solution:
Toolbar.expandItemActionView checks if the ActionView implements CollapsibleActionView interface and calls CollapsibleActionView.onActionViewExpanded method if this is the case. This is your chance to fix the layout paramters and set the width to match_parent. Derive a custom class from the SeekBar and let it implement CollapsibleActionView:
public class CollapsibleSeekBar extends AppCompatSeekBar implements CollapsibleActionView {
public CollapsibleSeekBar(Context context) {
super(context);
}
public CollapsibleSeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CollapsibleSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#Override
public void onActionViewExpanded() {
ViewGroup.LayoutParams params = getLayoutParams();
params.width = ViewGroup.LayoutParams.MATCH_PARENT;
setLayoutParams(params);
requestLayout();
}
#Override
public void onActionViewCollapsed() {
}
}
Use it in your menu item via app:actionViewClass.
Also make sure to use the appropriate CollapsibleActionView.
If you are using android.support.v4.widget.Toolbar you also need android.support.v7.view.CollapsibleActionView.
If you have an android.widget.Toolbar use android.view.CollapsibleActionView
I'm trying to figure out how the expand/collapse animation of the toolbar is done. If you have a look at the Telegram app settings, you will see that there is a listview and the toolbar. When you scroll down, the toolbar collapse, and when you scroll up it expands. There is also the animation of the profile pic and the FAB. Does anyone have any clue on that? Do you think they built all the animations on top of it? Maybe I'm missing something from the new APIs or the support library.
I noticed the same behaviour on the Google calendar app, when you open the Spinner (I don't think it's a spinner, but it looks like): The toolbar expands and when you scroll up, it collapse.
Just to clearify: I don't need the QuickReturn method. I know that probably Telegram app is using something similar. The exact method that I need is the Google Calendar app effect. I've tried with
android:animateLayoutChanges="true"
and the expand method works pretty well. But obviously, If I scroll up the ListView, the toolbar doesn't collapse.
I've also thought about adding a GestureListener but I want to know if there are any APIs or simpler methods of achieving this.
If there are none, I think I will go with the GestureListener. Hopefully to have a smooth effect of the Animation.
Thanks!
Edit :
Since the release of the Android Design support library, there's an easier solution. Check joaquin's answer
--
Here's how I did it, there probably are many other solutions but this one worked for me.
First of all, you have to use a Toolbar with a transparent background. The expanding & collapsing Toolbar is actually a fake one that's under the transparent Toolbar. (you can see on the first screenshot below - the one with the margins - that this is also how they did it in Telegram).
We only keep the actual Toolbar for the NavigationIcon and the overflow MenuItem.
Everything that's in the red rectangle on the second screenshot (ie the fake Toolbar and the FloatingActionButton) is actually a header that you add to the settings ListView (or ScrollView).
So you have to create a layout for this header in a separate file that could look like this :
<!-- The headerView layout. Includes the fake Toolbar & the FloatingActionButton -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:id="#+id/header_container"
android:layout_width="match_parent"
android:layout_height="#dimen/header_height"
android:layout_marginBottom="3dp"
android:background="#android:color/holo_blue_dark">
<RelativeLayout
android:id="#+id/header_infos_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:padding="16dp">
<ImageView
android:id="#+id/header_picture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginRight="8dp"
android:src="#android:drawable/ic_dialog_info" />
<TextView
android:id="#+id/header_title"
style="#style/TextAppearance.AppCompat.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="#+id/header_picture"
android:text="Toolbar Title"
android:textColor="#android:color/white" />
<TextView
android:id="#+id/header_subtitle"
style="#style/TextAppearance.AppCompat.Subhead"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#+id/header_title"
android:layout_toRightOf="#+id/header_picture"
android:text="Toolbar Subtitle"
android:textColor="#android:color/white" />
</RelativeLayout>
</RelativeLayout>
<FloatingActionButton
android:id="#+id/header_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_margin="10dp"
android:src="#drawable/ic_open_in_browser"/>
</FrameLayout>
(Note that you can use negative margins/padding for the fab to be straddling on 2 Views)
Now comes the interesting part. In order to animate the expansion of our fake Toolbar, we implement the ListView onScrollListener.
// The height of your fully expanded header view (same than in the xml layout)
int headerHeight = getResources().getDimensionPixelSize(R.dimen.header_height);
// The height of your fully collapsed header view. Actually the Toolbar height (56dp)
int minHeaderHeight = getResources().getDimensionPixelSize(R.dimen.action_bar_height);
// The left margin of the Toolbar title (according to specs, 72dp)
int toolbarTitleLeftMargin = getResources().getDimensionPixelSize(R.dimen.toolbar_left_margin);
// Added after edit
int minHeaderTranslation;
private ListView listView;
// Header views
private View headerView;
private RelativeLayout headerContainer;
private TextView headerTitle;
private TextView headerSubtitle;
private FloatingActionButton headerFab;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View rootView = inflater.inflate(R.layout.listview_fragment, container, false);
listView = rootView.findViewById(R.id.listview);
// Init the headerHeight and minHeaderTranslation values
headerHeight = getResources().getDimensionPixelSize(R.dimen.header_height);
minHeaderTranslation = -headerHeight +
getResources().getDimensionPixelOffset(R.dimen.action_bar_height);
// Inflate your header view
headerView = inflater.inflate(R.layout.header_view, listview, false);
// Retrieve the header views
headerContainer = (RelativeLayout) headerView.findViewById(R.id.header_container);
headerTitle = (TextView) headerView.findViewById(R.id.header_title);
headerSubtitle = (TextView) headerView.findViewById(R.id.header_subtitle);
headerFab = (TextView) headerView.findViewById(R.id.header_fab);;
// Add the headerView to your listView
listView.addHeaderView(headerView, null, false);
// Set the onScrollListener
listView.setOnScrollListener(this);
// ...
return rootView;
}
#Override
public void onScrollStateChanged(AbsListView view, int scrollState)
{
// Do nothing
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
{
Integer scrollY = getScrollY(view);
// This will collapse the header when scrolling, until its height reaches
// the toolbar height
headerView.setTranslationY(Math.max(0, scrollY + minHeaderTranslation));
// Scroll ratio (0 <= ratio <= 1).
// The ratio value is 0 when the header is completely expanded,
// 1 when it is completely collapsed
float offset = 1 - Math.max(
(float) (-minHeaderTranslation - scrollY) / -minHeaderTranslation, 0f);
// Now that we have this ratio, we only have to apply translations, scales,
// alpha, etc. to the header views
// For instance, this will move the toolbar title & subtitle on the X axis
// from its original position when the ListView will be completely scrolled
// down, to the Toolbar title position when it will be scrolled up.
headerTitle.setTranslationX(toolbarTitleLeftMargin * offset);
headerSubtitle.setTranslationX(toolbarTitleLeftMargin * offset);
// Or we can make the FAB disappear when the ListView is scrolled
headerFab.setAlpha(1 - offset);
}
// Method that allows us to get the scroll Y position of the ListView
public int getScrollY(AbsListView view)
{
View c = view.getChildAt(0);
if (c == null)
return 0;
int firstVisiblePosition = view.getFirstVisiblePosition();
int top = c.getTop();
int headerHeight = 0;
if (firstVisiblePosition >= 1)
headerHeight = this.headerHeight;
return -top + firstVisiblePosition * c.getHeight() + headerHeight;
}
Note that there are some parts of this code I didn't test, so feel free to highlight mistakes. But overall, I'm know that this solution works, even though I'm sure it can be improved.
EDIT 2:
There were some mistakes in the code above (that I didn't test until today...), so I changed a few lines to make it work :
I introduced another variable, minHeaderTranslation, which replaced minHeaderHeight;
I changed the Y translation value applied to the header View from :
headerView.setTranslationY(Math.max(-scrollY, minHeaderTranslation));
to :
headerView.setTranslationY(Math.max(0, scrollY + minHeaderTranslation));
Previous expression wasn't working at all, I'm sorry about that...
The ratio calculation also changed, so that it now evolves from the bottom the toolbar (instead of the top of the screen) to the full expanded header.
Also check out CollapsingTitleLayout written by Chris Banes in Android team:
https://plus.google.com/+ChrisBanes/posts/J9Fwbc15BHN
Code: https://gist.github.com/chrisbanes/91ac8a20acfbdc410a68
Use design support library http://android-developers.blogspot.in/2015/05/android-design-support-library.html
include this in build.gradle
compile 'com.android.support:design:22.2.0'
compile 'com.android.support:appcompat-v7:22.2.+'
for recycler view include this also
compile 'com.android.support:recyclerview-v7:22.2.0'
<!-- AppBarLayout allows your Toolbar and other views (such as tabs provided by TabLayout)
to react to scroll events in a sibling view marked with a ScrollingViewBehavior.-->
<android.support.design.widget.AppBarLayout
android:id="#+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<!-- specify tag app:layout_scrollFlags -->
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"/>
<!-- specify tag app:layout_scrollFlags -->
<android.support.design.widget.TabLayout
android:id="#+id/tabLayout"
android:scrollbars="horizontal"
android:layout_below="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"/>
<!-- app:layout_collapseMode="pin" will help to pin this view at top when scroll -->
<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="Title"
android:gravity="center"
app:layout_collapseMode="pin" />
</android.support.design.widget.AppBarLayout>
<!-- This will be your scrolling view.
app:layout_behavior="#string/appbar_scrolling_view_behavior" tag connects this features -->
<android.support.v7.widget.RecyclerView
android:id="#+id/list"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.v7.widget.RecyclerView>
</android.support.design.widget.CoordinatorLayout>
Your activity should extend AppCompatActivity
public class YourActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.your_layout);
//set toolbar
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
}
}
Your app theme should be like this
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.NoActionBar">
</style>
</resources>
This is my implementation:
collapsedHeaderHeight and expandedHeaderHeight are defined somewhere else, with the function getAnimationProgress I can get the Expand/Collapse progress, base on this value I do my animation and show/hide the real header.
listForumPosts.setOnScrollListener(new AbsListView.OnScrollListener() {
/**
* #return [0,1], 0 means header expanded, 1 means header collapsed
*/
private float getAnimationProgress(AbsListView view, int firstVisibleItem) {
if (firstVisibleItem > 0)
return 1;
// should not exceed 1
return Math.min(
-view.getChildAt(0).getTop() / (float) (expandedHeaderHeight - collapsedHeaderHeight), 1);
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
// at render beginning, the view could be empty!
if (view.getChildCount() > 0) {
float animationProgress = getAnimationProgress(view, firstVisibleItem);
imgForumHeaderAvatar.setAlpha(1-animationProgress);
if (animationProgress == 1) {
layoutForumHeader.setVisibility(View.VISIBLE);
} else {
layoutForumHeader.setVisibility(View.GONE);
}
}
}
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
// do nothing
}
}
my question is simple. Can I use an HorizontalScrollView inside the content menu of a DrawerLayout?
My DrawerLayout looks like this:
<android.support.v4.widget.DrawerLayout
android:id="#+id/pnlMenu"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<!-- Main content view -->
<ListView
android:id="#+id/lst"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:fastScrollEnabled="true"
android:focusable="true"
android:focusableInTouchMode="true"
tools:listitem="#layout/item_layout" >
</ListView>
<!-- Content of menu -->
<FrameLayout
android:id="#+id/drawerFrame"
android:layout_width="300dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:clickable="true"
android:background="#color/black" >
<fragment
android:id="#+id/frag"
android:layout_width="match_parent"
android:layout_height="match_parent"
class="com.test.TestFragment" />
</FrameLayout>
</android.support.v4.widget.DrawerLayout>
Inside the fragment I have the HorizontalScrollView but when I try to touch it nothing happen because the drawer layout follow my finger.
I think that disabling the touch events inside the content menu and make DrawerLayout closable only when main content view is clicked will solve my problem. Is that true? If not, can someone tell me what can I do?
Thank you.
Based on this solution: https://stackoverflow.com/a/7258579/452486
I've been able to make HorizontalScrollView scrollable.
Create a class extends DrawerLayout:
public class AllowChildInterceptTouchEventDrawerLayout extends DrawerLayout {
private int mInterceptTouchEventChildId;
public void setInterceptTouchEventChildId(int id) {
this.mInterceptTouchEventChildId = id;
}
public AllowChildInterceptTouchEventDrawerLayout(Context context) {
super(context);
}
public AllowChildInterceptTouchEventDrawerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mInterceptTouchEventChildId > 0) {
View scroll = findViewById(mInterceptTouchEventChildId);
if (scroll != null) {
Rect rect = new Rect();
scroll.getHitRect(rect);
if (rect.contains((int) ev.getX(), (int) ev.getY())) {
return false;
}
}
}
return super.onInterceptTouchEvent(ev);
}
}
And add the child id which you want to intercept the touch event of drawerlayout
AllowChildInterceptTouchEventDrawerLayout drawerLayout = (AllowChildInterceptTouchEventDrawerLayout) findViewById(R.id.layoutdrawer_id);
drawerLayout.setInterceptTouchEventChildId(R.id.horizontalscrollview_id);
It's better to set requestDisallowInterceptTouchEvent(true) flag instead of returning false. When We have a HorizontalScrollView and below it there's another view (eg. ListView with some menu items) and we make a dynamic gesture from HorizontalScrollView area to the aforementioned ListView the app will crash with a NPE. This case happens when you open the app for the first time and go to a DrawerLayout through the hamburger icon. Use this:
if (rect.contains((int) ev.getX(), (int) ev.getY())) {
this.requestDisallowInterceptTouchEvent(true);
}
If you want to have HorizontalScrollView inside your DrawerLayout an alternative strategy to all the answers so far would be to lock the drawer when opened. That means user won't be able to close the drawer with a swipe but the scrolling inside HorizontalScrollView will work as expected.
The locking is achieved by calling
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_OPEN);
A convenient place for this call would be in onDrawerOpened.
Unfortunately the locking also prevents drawer from closing when user taps the scrim (the dimmed part of the screen not occupied by drawer). You'll need to catch that tap and close it yourself, with something like this:
mDrawerLayout.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
int x = (int) event.getX();
int drawerWidth = (int)getResources().getDimension(R.dimen.your_drawer_width);
if (x > drawerWidth) {
// inside scrim
mDrawerLayout.closeDrawer();
return true;
}
return false;
}
});
I want to create a custom Compound Control in Android that holds some logic. For the purpose of this example, let's say I want it to switch between two views when clicked.
According to the API guide, it looks like the way to do that is to create a new class that extends Layout, and do everything in there.
So I did just that:
I created a XML layout to inflate for my custom component:
.
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/view1"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="Hello"/>
<TextView
android:id="#+id/view2"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="World"
android:visibility="gone"/>
</RelativeLayout>
Then I created my custom Layout class, and added the logic in there:
public class MyWidget extends RelativeLayout {
public final View mView1;
public final View mView2;
public MyWidget(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
RelativeLayout view = (RelativeLayout) inflater.inflate(R.layout.my_widget, this, true);
mView1 = view.findViewById(R.id.view1);
mView2 = view.findViewById(R.id.view2);
view.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
switchViews();
}
});
}
public void switchViews() {
if (mView1.getVisibility() == View.VISIBLE) {
mView1.setVisibility(View.GONE);
} else {
mView1.setVisibility(View.VISIBLE);
}
if (mView2.getVisibility() == View.VISIBLE) {
mView2.setVisibility(View.GONE);
} else {
mView2.setVisibility(View.VISIBLE);
}
}
}
And finally, I included my custom view in some layout:
.
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.example.MyWidget
android:layout_height="match_parent"
android:layout_width="match_parent"/>
</RelativeLayout
And that works.
I am not completely happy with that solution though, for 2 reasons:
In the constructor of MyWidget, I instantiate 2 nested RelativeLayout by calling the super() constructor, and the one that is at the root of my XML layout. For that, I know I can instead use <merge> as my XML root and that gets me rid of the extra RelativeLayout. Except that defining XML attributes, such as android:background on my <merge> tag doesn't have any effect, so I have to define it programmatically, which is not as nice.
The custom View is a subclass of RelativeLayout, and therefore expose methods it inherits from it, such as addView(), even if it doesn't make sense to add child views to it. I know I can override those methods to prevent users from doing that, but I would still find it cleaner to inherit from View.