Skip to content

Instantly share code, notes, and snippets.

@chrisbanes
Created February 19, 2014 13:16
Show Gist options
  • Save chrisbanes/9091754 to your computer and use it in GitHub Desktop.
Save chrisbanes/9091754 to your computer and use it in GitHub Desktop.
ForegroundLinearLayout
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ForegroundLinearLayout">
<attr name="android:foreground" />
<attr name="android:foregroundInsidePadding" />
<attr name="android:foregroundGravity" />
</declare-styleable>
</resources>
/*
* Copyright (C) 2006 The Android Open Source Project
*
* 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 your.package;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.widget.LinearLayout;
import your.package.R;
public class ForegroundLinearLayout extends LinearLayout {
private Drawable mForeground;
private final Rect mSelfBounds = new Rect();
private final Rect mOverlayBounds = new Rect();
private int mForegroundGravity = Gravity.FILL;
protected boolean mForegroundInPadding = true;
boolean mForegroundBoundsChanged = false;
public ForegroundLinearLayout(Context context) {
super(context);
}
public ForegroundLinearLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ForegroundLinearLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ForegroundLinearLayout,
defStyle, 0);
mForegroundGravity = a.getInt(
R.styleable.ForegroundLinearLayout_android_foregroundGravity, mForegroundGravity);
final Drawable d = a.getDrawable(R.styleable.ForegroundLinearLayout_android_foreground);
if (d != null) {
setForeground(d);
}
mForegroundInPadding = a.getBoolean(
R.styleable.ForegroundLinearLayout_android_foregroundInsidePadding, true);
a.recycle();
}
/**
* Describes how the foreground is positioned.
*
* @return foreground gravity.
*
* @see #setForegroundGravity(int)
*/
public int getForegroundGravity() {
return mForegroundGravity;
}
/**
* Describes how the foreground is positioned. Defaults to START and TOP.
*
* @param foregroundGravity See {@link android.view.Gravity}
*
* @see #getForegroundGravity()
*/
public void setForegroundGravity(int foregroundGravity) {
if (mForegroundGravity != foregroundGravity) {
if ((foregroundGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) {
foregroundGravity |= Gravity.START;
}
if ((foregroundGravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
foregroundGravity |= Gravity.TOP;
}
mForegroundGravity = foregroundGravity;
if (mForegroundGravity == Gravity.FILL && mForeground != null) {
Rect padding = new Rect();
mForeground.getPadding(padding);
}
requestLayout();
}
}
@Override
protected boolean verifyDrawable(Drawable who) {
return super.verifyDrawable(who) || (who == mForeground);
}
@Override
public void jumpDrawablesToCurrentState() {
super.jumpDrawablesToCurrentState();
if (mForeground != null) mForeground.jumpToCurrentState();
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
if (mForeground != null && mForeground.isStateful()) {
mForeground.setState(getDrawableState());
}
}
/**
* Supply a Drawable that is to be rendered on top of all of the child
* views in the frame layout. Any padding in the Drawable will be taken
* into account by ensuring that the children are inset to be placed
* inside of the padding area.
*
* @param drawable The Drawable to be drawn on top of the children.
*/
public void setForeground(Drawable drawable) {
if (mForeground != drawable) {
if (mForeground != null) {
mForeground.setCallback(null);
unscheduleDrawable(mForeground);
}
mForeground = drawable;
if (drawable != null) {
setWillNotDraw(false);
drawable.setCallback(this);
if (drawable.isStateful()) {
drawable.setState(getDrawableState());
}
if (mForegroundGravity == Gravity.FILL) {
Rect padding = new Rect();
drawable.getPadding(padding);
}
} else {
setWillNotDraw(true);
}
requestLayout();
invalidate();
}
}
/**
* Returns the drawable used as the foreground of this FrameLayout. The
* foreground drawable, if non-null, is always drawn on top of the children.
*
* @return A Drawable or null if no foreground was set.
*/
public Drawable getForeground() {
return mForeground;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
mForegroundBoundsChanged = changed;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mForegroundBoundsChanged = true;
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (mForeground != null) {
final Drawable foreground = mForeground;
if (mForegroundBoundsChanged) {
mForegroundBoundsChanged = false;
final Rect selfBounds = mSelfBounds;
final Rect overlayBounds = mOverlayBounds;
final int w = getRight() - getLeft();
final int h = getBottom() - getTop();
if (mForegroundInPadding) {
selfBounds.set(0, 0, w, h);
} else {
selfBounds.set(getPaddingLeft(), getPaddingTop(),
w - getPaddingRight(), h - getPaddingBottom());
}
Gravity.apply(mForegroundGravity, foreground.getIntrinsicWidth(),
foreground.getIntrinsicHeight(), selfBounds, overlayBounds);
foreground.setBounds(overlayBounds);
}
foreground.draw(canvas);
}
}
}
@dvdh
Copy link

dvdh commented Mar 6, 2014

Hey what's the purpose of calling getPadding on lines 104 and 155?

@suau
Copy link

suau commented Mar 17, 2014

@dvdh it's ported from FrameLayout, which has the methods:

int getPaddingLeftWithForeground()
int getPadding****WithForeground() etc.

it has no use here, you can remove it if you don't plan to add these methods to your custom view.

@suau
Copy link

suau commented Mar 17, 2014

Note there is a bug, which prevents this to work in some cases, e.g. in a DrawerLayout.
This happens when onLayout is called with changed = false before draw(Canvas canvas) was called. Therefore setBounds never gets called on the foregroundDrawable.

Here is a simple fix (i think there are no pull requests for gists)
replace line 178:
mForegroundBoundsChanged = changed;

if (changed) {
   mForegroundBoundsChanged = true;
}

@dvdh
Copy link

dvdh commented Mar 24, 2014

Thanks suau, that cleared it up!

@gabrielemariotti
Copy link

With Android 5.0 the ripple drawable will animate in the foreground, but the ripple will always begin at the center of the view.

A solution can be:

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    @Override
    public boolean onTouchEvent(MotionEvent e) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            if (e.getActionMasked() == MotionEvent.ACTION_DOWN) {
                if (mForeground != null)
                    mForeground.setHotspot(e.getX(), e.getY());
            }
        }
        return super.onTouchEvent(e);
    }

@AndroidDeveloperLB
Copy link

@gabrielemariotti Thank you for this solution.

@NightlyNexus
Copy link

For travelers, see the better solution by @gabrielemariotti in commit f5b5e4d6c87a247aff5b477cea13859338f813fa.

https://github.com/gabrielemariotti/cardslib/blob/master/library-core/src/main/java/it/gmariotti/cardslib/library/view/ForegroundLinearLayout.java

He has since corrected the logic to drawableHotspotChanged(float x, float y).

@Pkmmte
Copy link

Pkmmte commented Jun 8, 2015

Using a RippleDrawable as the background causes it to bleed out of the layout bounds. Setting it to the foreground works fine, however.

Anyone know of a fix for this?

@dluco
Copy link

dluco commented Dec 17, 2015

The simplest fix would probably be to give the ripple a mask so that it's clipped by the view's bounds.

@altaf933
Copy link

altaf933 commented Sep 2, 2016

minSdkVersion 17 it's getting crash and the log cat error "Caused by: java.lang.NoSuchMethodError: android.widget.LinearLayout.".

It might be not support for lower version ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment