Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Snackbar control #165

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
327 changes: 327 additions & 0 deletions MaterialSkin/Controls/MaterialSnackbar.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,327 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using MaterialSkin.Animations;
using System.Drawing.Text;
using System.Drawing.Drawing2D;

namespace MaterialSkin.Controls
{
public class MaterialSnackbar : Control, IMaterialControl
{
[Browsable(false)]
public int Depth { get; set; }
[Browsable(false)]
public MaterialSkinManager SkinManager => MaterialSkinManager.Instance;
[Browsable(false)]
public MouseState MouseState { get; set; }

private AnimationManager _animationManager;

private MaterialLabel _textLabel;
private MaterialSnackbarFlatButton _button;
private Timer _idleTimer;
private bool _isShowing = false;
private EventHandler _currentClickHandler;

public override string Text
{
get => _textLabel.Text;
set => _textLabel.Text = value;
}
public string ButtonText
{
get => _button.Text;
set => _button.Text = value;
}

public bool ShowButton
{
get => _button.Visible;
set => _button.Visible = value;
}

public EventHandler OnButtonClick
{
set => SetClickEventHandler(value);
}

private const int SNACKBAR_HEIGHT = 48;
private const int SNACKBAR_PADDING_LEFT_RIGHT = 24;
private const int SNACKBAR_LABEL_PADDING_TOP_BOTTOM = 14;
private const int SNACKBAR_BUTTON_PADDING_TOP_BOTTOM = 7;
private const int SNACKBAR_DURATION_LONG = 3500;
private const int SNACKBAR_DURATION_SHORT = 2000;
private const int SNACKBAR_MIN_WIDTH = 288;
private const int SNACKBAR_MAX_WIDTH = 568;
private const int SNACKBAR_CHAR_BREAK_POSITION = 67;

public MaterialSnackbar()
{
_animationManager = new AnimationManager
{
Increment = 0.03,
AnimationType = AnimationType.EaseOut
};
_animationManager.OnAnimationProgress += sender => Invalidate();
Invalidated += MaterialSnackbar_Invalidated;

_animationManager.OnAnimationFinished += _animationManager_OnAnimationFinished;

Visible = false;
_textLabel = new MaterialLabel();
Controls.Add(_textLabel);
_textLabel.Location = new Point(SNACKBAR_PADDING_LEFT_RIGHT, SNACKBAR_LABEL_PADDING_TOP_BOTTOM);
_textLabel.Font = SkinManager.ROBOTO_REGULAR_14;
_textLabel.BackColor = SkinManager.GetSnackbarBackgroundColor();
_textLabel.ForeColor = SkinManager.GetSnackbarTextColor();

_button = new MaterialSnackbarFlatButton();
Controls.Add(_button);
_button.Visible = false;
_button.Click += _button_Click;

_idleTimer = new Timer()
{
Interval = SNACKBAR_DURATION_SHORT
};
_idleTimer.Tick += _idleTimer_Tick;
this.DoubleBuffered = true;
}

private void _button_Click(object sender, EventArgs e)
{
_animationManager.StartNewAnimation(AnimationDirection.Out);
_isShowing = true;
}

private void SetClickEventHandler(EventHandler handler)
{
if(_currentClickHandler != null)
{
_button.Click -= _currentClickHandler;
}
_button.Click += handler;
_currentClickHandler = handler;
}

private void MaterialSnackbar_Invalidated(object sender, InvalidateEventArgs e)
{
Size = new Size(Parent.Width, SNACKBAR_HEIGHT);
int textWidth = Convert.ToInt32(CreateGraphics().MeasureString(_textLabel.Text, _textLabel.Font).Width);
int requiredWidth = SNACKBAR_PADDING_LEFT_RIGHT * 3 + textWidth + _button.Width;
if(requiredWidth < SNACKBAR_MIN_WIDTH)
{
Size = new Size(SNACKBAR_MIN_WIDTH, SNACKBAR_HEIGHT);
}
else if(requiredWidth > SNACKBAR_MAX_WIDTH)
{
Size = new Size(SNACKBAR_MAX_WIDTH, SNACKBAR_HEIGHT);
_textLabel.Text = _textLabel.Text.Substring(0, SNACKBAR_CHAR_BREAK_POSITION) + "...";
}
else
{
Size = new Size(requiredWidth, SNACKBAR_HEIGHT);
}
_textLabel.Width = textWidth;
_textLabel.ForeColor = SkinManager.GetSnackbarTextColor();
_button.Location = new Point(Width - _button.Width - SNACKBAR_PADDING_LEFT_RIGHT, SNACKBAR_BUTTON_PADDING_TOP_BOTTOM);

if (_animationManager.IsAnimating())
{
var value = _animationManager.GetProgress();
Location = new Point((Parent.Width / 2) - (Width / 2), Parent.Height - Convert.ToInt32((SNACKBAR_HEIGHT * value)));

}
}

private void _idleTimer_Tick(object sender, EventArgs e)
{
_idleTimer.Stop();
_animationManager.StartNewAnimation(AnimationDirection.Out);
_isShowing = true;
}

private void _animationManager_OnAnimationFinished(object sender)
{
if(_isShowing)
{
_isShowing = false;
Visible = false;
}
else
{
if (!_button.Visible)
{
_idleTimer.Start();
}
}
}

public void ShowSnackbar(int duration = SNACKBAR_DURATION_SHORT)
{
if(!Visible)
{
_idleTimer.Interval = duration;
_animationManager.StartNewAnimation(AnimationDirection.In);
Visible = true;
}
}

protected override void OnPaint(PaintEventArgs e)
{
Graphics g = e.Graphics;
using (var path = DrawHelper.CreateRoundRect(ClientRectangle.X, ClientRectangle.Y, ClientRectangle.Width, ClientRectangle.Height, 1f))
{
g.FillPath(SkinManager.GetSnackbarBackgroundBrush(), path);
}
}
}

// This is the only way to get the correct background color for the button AND keep the rounded edges on the snackbar.
internal class MaterialSnackbarFlatButton : MaterialFlatButton
{
private readonly AnimationManager _animationManager;
private readonly AnimationManager _hoverAnimationManager;

private SizeF _textSize;

private Image _icon;

public MaterialSnackbarFlatButton()
{
Primary = false;

_animationManager = new AnimationManager(false)
{
Increment = 0.03,
AnimationType = AnimationType.EaseOut
};
_hoverAnimationManager = new AnimationManager
{
Increment = 0.07,
AnimationType = AnimationType.Linear
};

_hoverAnimationManager.OnAnimationProgress += sender => Invalidate();
_animationManager.OnAnimationProgress += sender => Invalidate();

AutoSizeMode = AutoSizeMode.GrowAndShrink;
AutoSize = true;
Margin = new Padding(4, 6, 4, 6);
Padding = new Padding(0);
}

protected override void OnPaint(PaintEventArgs pevent)
{
var g = pevent.Graphics;
g.TextRenderingHint = TextRenderingHint.AntiAlias;

g.Clear(SkinManager.GetSnackbarBackgroundColor());

//Hover
Color c = SkinManager.GetSnackbarBackgroundColor();
using (Brush b = new SolidBrush(Color.FromArgb((int)(_hoverAnimationManager.GetProgress() * c.A), c.RemoveAlpha())))
g.FillRectangle(b, ClientRectangle);

//Ripple
if (_animationManager.IsAnimating())
{
g.SmoothingMode = SmoothingMode.AntiAlias;
for (var i = 0; i < _animationManager.GetAnimationCount(); i++)
{
var animationValue = _animationManager.GetProgress(i);
var animationSource = _animationManager.GetSource(i);

using (Brush rippleBrush = new SolidBrush(Color.FromArgb((int)(101 - (animationValue * 100)), Color.Black)))
{
var rippleSize = (int)(animationValue * Width * 2);
g.FillEllipse(rippleBrush, new Rectangle(animationSource.X - rippleSize / 2, animationSource.Y - rippleSize / 2, rippleSize, rippleSize));
}
}
g.SmoothingMode = SmoothingMode.None;
}

//Icon
var iconRect = new Rectangle(8, 6, 24, 24);

if (string.IsNullOrEmpty(Text))
// Center Icon
iconRect.X += 2;

if (Icon != null)
g.DrawImage(Icon, iconRect);

//Text
var textRect = ClientRectangle;

if (Icon != null)
{
//
// Resize and move Text container
//

// First 8: left padding
// 24: icon width
// Second 4: space between Icon and Text
// Third 8: right padding
textRect.Width -= 8 + 24 + 4 + 8;

// First 8: left padding
// 24: icon width
// Second 4: space between Icon and Text
textRect.X += 8 + 24 + 4;
}

g.DrawString(
Text.ToUpper(),
SkinManager.ROBOTO_MEDIUM_10,
Enabled ? (Primary ? SkinManager.ColorScheme.PrimaryBrush : SkinManager.GetSnackbarTextBrush()) : SkinManager.GetFlatButtonDisabledTextBrush(),
textRect,
new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }
);
}

protected override void OnCreateControl()
{
base.OnCreateControl();
if (DesignMode) return;

MouseState = MouseState.OUT;
MouseEnter += (sender, args) =>
{
MouseState = MouseState.HOVER;
_hoverAnimationManager.StartNewAnimation(AnimationDirection.In);
Invalidate();
};
MouseLeave += (sender, args) =>
{
MouseState = MouseState.OUT;
_hoverAnimationManager.StartNewAnimation(AnimationDirection.Out);
Invalidate();
};
MouseDown += (sender, args) =>
{
if (args.Button == MouseButtons.Left)
{
MouseState = MouseState.DOWN;

_animationManager.StartNewAnimation(AnimationDirection.In, args.Location);
Invalidate();
}
};
MouseUp += (sender, args) =>
{
MouseState = MouseState.HOVER;

Invalidate();
};
}
}
}
28 changes: 28 additions & 0 deletions MaterialSkin/MaterialSkinManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ public enum Themes : byte
private static readonly Color FLAT_BUTTON_DISABLEDTEXT_DARK = Color.FromArgb(30.PercentageToColorComponent(), 0xFFFFFF.ToColor());
private static readonly Brush FLAT_BUTTON_DISABLEDTEXT_DARK_BRUSH = new SolidBrush(FLAT_BUTTON_DISABLEDTEXT_DARK);

//Snackbar
private static readonly Color SNACKBAR_BACKGROUND = Color.FromArgb(255, 50, 50, 50);
private static readonly Brush SNACKBAR_BACKGROUND_BRUSH = new SolidBrush(SNACKBAR_BACKGROUND);
private static readonly Color SNACKBAR_TEXT_COLOR = PRIMARY_TEXT_WHITE;
private static readonly Brush SNACKBAR_TEXT_BRUSH = new SolidBrush(SNACKBAR_TEXT_COLOR);

//ContextMenuStrip
private static readonly Color CMS_BACKGROUND_LIGHT_HOVER = Color.FromArgb(255, 238, 238, 238);
private static readonly Brush CMS_BACKGROUND_HOVER_LIGHT_BRUSH = new SolidBrush(CMS_BACKGROUND_LIGHT_HOVER);
Expand Down Expand Up @@ -225,7 +231,28 @@ public Color GetApplicationBackgroundColor()
return Theme == Themes.LIGHT ? BACKGROUND_LIGHT : BACKGROUND_DARK;
}

public Color GetSnackbarBackgroundColor()
{
return SNACKBAR_BACKGROUND;
}

public Brush GetSnackbarBackgroundBrush()
{
return SNACKBAR_BACKGROUND_BRUSH;
}

public Color GetSnackbarTextColor()
{
return SNACKBAR_TEXT_COLOR;
}

public Brush GetSnackbarTextBrush()
{
return SNACKBAR_TEXT_BRUSH;
}

//Roboto font
public Font ROBOTO_REGULAR_14;
public Font ROBOTO_MEDIUM_12;
public Font ROBOTO_REGULAR_11;
public Font ROBOTO_MEDIUM_11;
Expand All @@ -242,6 +269,7 @@ private MaterialSkinManager()
ROBOTO_MEDIUM_12 = new Font(LoadFont(Resources.Roboto_Medium), 12f);
ROBOTO_MEDIUM_10 = new Font(LoadFont(Resources.Roboto_Medium), 10f);
ROBOTO_REGULAR_11 = new Font(LoadFont(Resources.Roboto_Regular), 11f);
ROBOTO_REGULAR_14 = new Font(LoadFont(Resources.Roboto_Regular), 14f);
ROBOTO_MEDIUM_11 = new Font(LoadFont(Resources.Roboto_Medium), 11f);
Theme = Themes.LIGHT;
ColorScheme = new ColorScheme(Primary.BlueGrey800, Primary.BlueGrey900, Primary.BlueGrey500, Accent.LightBlue200, TextShade.WHITE);
Expand Down