unit customdrawn_android; {$mode objfpc}{$H+} interface uses // RTL Classes, SysUtils, Types, Math, // fpimage fpcanvas, fpimgcanv, fpimage, // LCL -> Use only TForm, TWinControl, TCanvas and TLazIntfImage Graphics, Controls, LCLType, LCLIntf, IntfGraphics, LResources, // customdrawndrawers, customdrawn_common; type { TCDDrawerAndroid } TCDDrawerAndroid = class(TCDDrawerCommon) private bmpCheckbox, bmpCheckboxChecked: TBitmap; // Draws a line alternating between two colors procedure DrawAndroidAlternatedHorzLine(ADest: TCanvas; X1, X2, Y: Integer; AColor1, AColor2: TColor); // Draws 2 mixed gradients which alternate between one another on each pixel procedure DrawAndroidMixedVertGradientFill(ADest: TCanvas; ARect: TRect; AStart1, AStop1, AStart2, AStop2: TColor); // Fills a rectangular area alternating between two pixels procedure DrawAndroidMixedFill(ADest: TCanvas; ARect: TRect; AColor1, AColor2: TColor); public procedure CreateResources; override; procedure LoadResources; override; procedure FreeResources; override; //procedure LoadFallbackPaletteColors; override; function GetDrawStyle: TCDDrawStyle; override; // General function GetMeasures(AMeasureID: Integer): Integer; override; { function GetMeasuresEx(ADest: TCanvas; AMeasureID: Integer; AState: TCDControlState; AStateEx: TCDControlStateEx): Integer; virtual; abstract; procedure CalculatePreferredSize(ADest: TCanvas; AControlId: TCDControlID; AState: TCDControlState; AStateEx: TCDControlStateEx; var PreferredWidth, PreferredHeight: integer; WithThemeSpace: Boolean); virtual; abstract; function GetColor(AColorID: Integer): TColor; virtual; abstract; function GetClientArea(ADest: TCanvas; ASize: TSize; AControlId: TCDControlID; AState: TCDControlState; AStateEx: TCDControlStateEx): TRect; virtual; abstract;} // General drawing routines {procedure DrawFocusRect(ADest: TCanvas; ADestPos: TPoint; ASize: TSize); override; procedure DrawRaisedFrame(ADest: TCanvas; ADestPos: TPoint; ASize: TSize); override; procedure DrawSunkenFrame(ADest: TCanvas; ADestPos: TPoint; ASize: TSize); override; procedure DrawShallowSunkenFrame(ADest: TCanvas; ADestPos: TPoint; ASize: TSize); override;} procedure DrawTickmark(ADest: TCanvas; ADestPos: TPoint); override; {procedure DrawSlider(ADest: TCanvas; ADestPos: TPoint; ASize: TSize; AState: TCDControlState); override; procedure DrawCompactArrow(ADest: TCanvas; ADestPos: TPoint; ADirection: TCDControlState); override;} // =================================== // Standard Tab // =================================== // TCDButton procedure DrawButton(ADest: TCanvas; ASize: TSize; AState: TCDControlState; AStateEx: TCDButtonStateEx); override; { // TCDEdit procedure DrawEditBackground(ADest: TCanvas; ADestPos: TPoint; ASize: TSize; AState: TCDControlState; AStateEx: TCDEditStateEx); override;} // TCDCheckBox procedure DrawCheckBoxSquare(ADest: TCanvas; ADestPos: TPoint; ASize: TSize; AState: TCDControlState; AStateEx: TCDControlStateEx); override; end; implementation const ANDROID_DPI = 'vldpi'; // Actually Android buttons are much more complex then this, // but this approximation works well ANDROID_BUTTON_CORNERS = $006B696B; ANDROID_BUTTON_FIRST_LINE_A = $00F7F3F7; ANDROID_BUTTON_FIRST_LINE_B = $00EFF3EF; ANDROID_BUTTON_SECOND_LINE_A = $00EFF3EF; ANDROID_BUTTON_SECOND_LINE_B = $00F7F3F7; ANDROID_BUTTON_THIRD_LINE_A = $00EFEFEF; ANDROID_BUTTON_THIRD_LINE_B = $00F7F3F7; ANDROID_BUTTON_TOP_GRADIENT_A = $00EFEFEF; ANDROID_BUTTON_TOP_GRADIENT_B = $00EFEBEF; ANDROID_BUTTON_MIDDLE_GRADIENT_A = $00CBCECB; ANDROID_BUTTON_MIDDLE_GRADIENT_B = $00CECFCE; ANDROID_BUTTON_BOTTOM_GRADIENT_A = $00BDBABD; ANDROID_BUTTON_BOTTOM_GRADIENT_B = $00BABDBA; ANDROID_BUTTON_PREPRELAST_LINE_A = $00C6C3C6; ANDROID_BUTTON_PREPRELAST_LINE_B = $00C6C3C6; ANDROID_BUTTON_PRELAST_LINE_A = $00C6CBC6; ANDROID_BUTTON_PRELAST_LINE_B = $00CECBCE; ANDROID_BUTTON_LAST_LINE_A = $00D6D3D6; ANDROID_BUTTON_LAST_LINE_B = $00D6D7D6; // Sunken variants ANDROID_BUTTON_SUNKEN_FIRST_LINE_A = $0066F366; ANDROID_BUTTON_SUNKEN_FIRST_LINE_B = $0066F366; ANDROID_BUTTON_SUNKEN_SECOND_LINE_A = $0066F366; ANDROID_BUTTON_SUNKEN_SECOND_LINE_B = $0066F366; ANDROID_BUTTON_SUNKEN_THIRD_LINE_A = $0066EF66; ANDROID_BUTTON_SUNKEN_THIRD_LINE_B = $0066F366; ANDROID_BUTTON_SUNKEN_TOP_GRADIENT_A = $0066EF66; ANDROID_BUTTON_SUNKEN_TOP_GRADIENT_B = $0066EB66; ANDROID_BUTTON_SUNKEN_MIDDLE_GRADIENT_A = $0033CE33; ANDROID_BUTTON_SUNKEN_MIDDLE_GRADIENT_B = $0033CF33; ANDROID_BUTTON_SUNKEN_BOTTOM_GRADIENT_A = $0000BA00; ANDROID_BUTTON_SUNKEN_BOTTOM_GRADIENT_B = $0000BD00; ANDROID_BUTTON_SUNKEN_PREPRELAST_LINE_A = $0000C300; ANDROID_BUTTON_SUNKEN_PREPRELAST_LINE_B = $0000C300; ANDROID_BUTTON_SUNKEN_PRELAST_LINE_A = $0000CB00; ANDROID_BUTTON_SUNKEN_PRELAST_LINE_B = $0000CB00; ANDROID_BUTTON_SUNKEN_LAST_LINE_A = $0000D300; ANDROID_BUTTON_SUNKEN_LAST_LINE_B = $0000D700; {procedure TCDButtonDrawerAndroid.DrawToIntfImage(ADest: TFPImageCanvas; CDButton: TCDButton); begin end; procedure TCDButtonDrawerAndroid.DrawToCanvas(ADest: TCanvas; CDButton: TCDButton); var //TmpB: TBitmap; Str: string; begin // Button shape -> This crashes in Gtk2 { TmpB.Canvas.Brush.Color := CDButton.Color; TmpB.Canvas.Brush.Style := bsSolid; TmpB.Canvas.RoundRect(0, 0, TmpB.Width, TmpB.Height, 8, 8); CDButton.SetShape(TmpB); ADest.Draw(0, 0, TmpB); TmpB.Free; } ADest.Brush.Color := CDButton.Parent.Color; ADest.Brush.Style := bsSolid; ADest.Pen.Color := ADest.Brush.Color; ADest.RecTangle(0, 0, CDButton.Width, CDButton.Height); // Button image if CDButton.IsDown then DrawCDButtonDown(ADest, CDButton.GetRGBBackgroundColor) else if CDButton.Focused then DrawAndroidButton(ADest, GetAColor(CDButton.Color, 98)) else DrawAndroidButton(ADest, GetAColor(CDButton.Color, 96)); // Button text ADest.Font.Assign(CDButton.Font); ADest.Brush.Style := bsClear; ADest.Pen.Style := psSolid; Str := CDButton.Caption; ADest.TextOut((CDButton.Width - ADest.TextWidth(Str)) div 2, (CDButton.Height - ADest.TextHeight(Str)) div 2, Str); end; initialization RegisterButtonDrawer(TCDButtonDrawerAndroid.Create, dsAndroid);} { TCDDrawerAndroid } procedure TCDDrawerAndroid.DrawAndroidAlternatedHorzLine(ADest: TCanvas; X1, X2, Y: Integer; AColor1, AColor2: TColor); var i: Integer; begin for i := X1 to X2-1 do begin if i mod 2 = 0 then ADest.Pixels[i, Y] := AColor1 else ADest.Pixels[i, Y] := AColor2; end; end; procedure TCDDrawerAndroid.DrawAndroidMixedVertGradientFill(ADest: TCanvas; ARect: TRect; AStart1, AStop1, AStart2, AStop2: TColor); var RStart1, RStop1, RStart2, RStop2: Byte; GStart1, GStop1, GStart2, GStop2: Byte; BStart1, BStop1, BStart2, BStop2: Byte; RDiff1, GDiff1, BDiff1: Integer; RDiff2, GDiff2, BDiff2: Integer; Count, I: Integer; lColor1, lColor2: TColor; begin if IsRectEmpty(ARect) then Exit; RedGreenBlue(ColorToRGB(AStart1), RStart1, GStart1, BStart1); RedGreenBlue(ColorToRGB(AStop1), RStop1, GStop1, BStop1); RedGreenBlue(ColorToRGB(AStart2), RStart2, GStart2, BStart2); RedGreenBlue(ColorToRGB(AStop2), RStop2, GStop2, BStop2); RDiff1 := RStop1 - RStart1; GDiff1 := GStop1 - GStart1; BDiff1 := BStop1 - BStart1; RDiff2 := RStop2 - RStart2; GDiff2 := GStop2 - GStart2; BDiff2 := BStop2 - BStart2; Count := ARect.Bottom - ARect.Top; for I := 0 to Count-1 do begin lColor1 := RGBToColor(RStart1 + (i * RDiff1) div Count, GStart1 + (i * GDiff1) div Count, BStart1 + (i * BDiff1) div Count); lColor2 := RGBToColor(RStart2 + (i * RDiff2) div Count, GStart2 + (i * GDiff2) div Count, BStart2 + (i * BDiff2) div Count); // draw left to right, because LineTo does not draw last pixel DrawAndroidAlternatedHorzLine(ADest, ARect.Left, ARect.Right, ARect.Top+I, lColor1, lColor2); end; end; procedure TCDDrawerAndroid.DrawAndroidMixedFill(ADest: TCanvas; ARect: TRect; AColor1, AColor2: TColor); var I: Integer; lColor1, lColor2: TColor; begin for I := 0 to (ARect.Bottom - ARect.Top)-1 do begin if i mod 2 = 0 then begin lColor1 := AColor1; lColor2 := AColor2; end else begin lColor1 := AColor2; lColor2 := AColor1; end; // draw left to right, because LineTo does not draw last pixel DrawAndroidAlternatedHorzLine(ADest, ARect.Left, ARect.Right, ARect.Top+I, lColor1, lColor2); end; end; procedure TCDDrawerAndroid.CreateResources; begin bmpCheckbox := TBitmap.Create; bmpCheckboxChecked := TBitmap.Create; end; procedure TCDDrawerAndroid.LoadResources; begin bmpCheckbox.LoadFromLazarusResource('android_checkbox'); bmpCheckboxChecked.LoadFromLazarusResource('android_checkbox_checked'); // for now hardcoded to ldpi ScaleRasterImage(bmpCheckbox, 160, 96); ScaleRasterImage(bmpCheckboxChecked, 160, 96); end; procedure TCDDrawerAndroid.FreeResources; begin bmpCheckbox.Free; bmpCheckboxChecked.Free; end; function TCDDrawerAndroid.GetDrawStyle: TCDDrawStyle; begin Result := dsAndroid; end; function TCDDrawerAndroid.GetMeasures(AMeasureID: Integer): Integer; begin case AMeasureID of { TCDEDIT_LEFT_TEXT_SPACING: Result := 4; TCDEDIT_RIGHT_TEXT_SPACING: Result := 3; TCDEDIT_TOP_TEXT_SPACING: Result := 3; TCDEDIT_BOTTOM_TEXT_SPACING: Result := 3;} // TCDCHECKBOX_SQUARE_HALF_HEIGHT: Result := 9; TCDCHECKBOX_SQUARE_HEIGHT: Result := 18; { // TCDRADIOBUTTON_CIRCLE_HEIGHT: Result := 15; // TCDSCROLLBAR_BUTTON_WIDTH: Result := 17; TCDSCROLLBAR_LEFT_SPACING: Result := 17; TCDSCROLLBAR_RIGHT_SPACING: Result := 17; TCDSCROLLBAR_LEFT_BUTTON_POS: Result := 0; TCDSCROLLBAR_RIGHT_BUTTON_POS: Result := -17; // TCDTRACKBAR_LEFT_SPACING: Result := 9; TCDTRACKBAR_RIGHT_SPACING: Result := 9; TCDTRACKBAR_TOP_SPACING: Result := 5; TCDTRACKBAR_FRAME_HEIGHT: Result := 17; // TCDLISTVIEW_COLUMN_LEFT_SPACING: Result := 10; TCDLISTVIEW_COLUMN_RIGHT_SPACING: Result := 10; TCDLISTVIEW_COLUMN_TEXT_LEFT_SPACING: Result := 5; TCDLISTVIEW_LINE_TOP_SPACING: Result := 3; TCDLISTVIEW_LINE_BOTTOM_SPACING: Result := 3;} else Result := inherited GetMeasures(AMeasureID); end; end; procedure TCDDrawerAndroid.DrawTickmark(ADest: TCanvas; ADestPos: TPoint); begin // Don't draw anything, tickmarks are impressed into the general images end; procedure TCDDrawerAndroid.DrawButton(ADest: TCanvas; ASize: TSize; AState: TCDControlState; AStateEx: TCDButtonStateEx); var Str: string; lGlyphLeftSpacing: Integer = 0; lTextOutPos: TPoint; lGlyphCaptionHeight: Integer; lColor: TColor; lRect: TRect; begin // Background corners lColor := AStateEx.ParentRGBColor; ADest.Pixels[0, 0] := lColor; ADest.Pixels[1, 0] := lColor; ADest.Pixels[0, 1] := lColor; ADest.Pixels[ASize.cx-1, 0] := lColor; ADest.Pixels[ASize.cx-2, 0] := lColor; ADest.Pixels[ASize.cx-1, 1] := lColor; ADest.Pixels[0, ASize.cy-1] := lColor; ADest.Pixels[1, ASize.cy-1] := lColor; ADest.Pixels[0, ASize.cy-2] := lColor; ADest.Pixels[ASize.cx-1, ASize.cy-1] := lColor; ADest.Pixels[ASize.cx-2, ASize.cy-1] := lColor; ADest.Pixels[ASize.cx-1, ASize.cy-2] := lColor; // Darker corners lColor := ANDROID_BUTTON_CORNERS; ADest.Pixels[1, 1] := lColor; ADest.Pixels[2, 0] := lColor; ADest.Pixels[0, 2] := lColor; ADest.Pixels[ASize.cx-3, 0] := lColor; ADest.Pixels[ASize.cx-2, 1] := lColor; ADest.Pixels[ASize.cx-1, 2] := lColor; ADest.Pixels[0, ASize.cy-3] := lColor; ADest.Pixels[1, ASize.cy-2] := lColor; ADest.Pixels[2, ASize.cy-1] := lColor; ADest.Pixels[ASize.cx-1, ASize.cy-3] := lColor; ADest.Pixels[ASize.cx-2, ASize.cy-2] := lColor; ADest.Pixels[ASize.cx-3, ASize.cy-1] := lColor; // Button image if csfSunken in AState then begin // Top lines DrawAndroidAlternatedHorzLine(ADest, 3, ASize.cx-3, 0, ANDROID_BUTTON_SUNKEN_FIRST_LINE_A, ANDROID_BUTTON_SUNKEN_FIRST_LINE_B); DrawAndroidAlternatedHorzLine(ADest, 2, ASize.cx-2, 1, ANDROID_BUTTON_SUNKEN_SECOND_LINE_A, ANDROID_BUTTON_SUNKEN_SECOND_LINE_B); DrawAndroidAlternatedHorzLine(ADest, 1, ASize.cx-1, 2, ANDROID_BUTTON_SUNKEN_THIRD_LINE_A, ANDROID_BUTTON_SUNKEN_THIRD_LINE_B); // The central gradient lRect := Bounds(0, 3, ASize.cx, (ASize.cy-6) div 3+1); DrawAndroidMixedVertGradientFill(ADest, lRect, ANDROID_BUTTON_SUNKEN_TOP_GRADIENT_A, ANDROID_BUTTON_SUNKEN_MIDDLE_GRADIENT_A, ANDROID_BUTTON_SUNKEN_TOP_GRADIENT_B, ANDROID_BUTTON_SUNKEN_MIDDLE_GRADIENT_B); lRect := Bounds(0, 3+(ASize.cy-6) div 3, ASize.cx, (ASize.cy-6) div 3+1); DrawAndroidMixedFill(ADest, lRect, ANDROID_BUTTON_SUNKEN_MIDDLE_GRADIENT_A, ANDROID_BUTTON_SUNKEN_MIDDLE_GRADIENT_B); lRect := Bounds(0, 3+2*(ASize.cy-6) div 3, ASize.cx, (ASize.cy-6) div 3+1); DrawAndroidMixedVertGradientFill(ADest, lRect, ANDROID_BUTTON_SUNKEN_MIDDLE_GRADIENT_A, ANDROID_BUTTON_SUNKEN_BOTTOM_GRADIENT_A, ANDROID_BUTTON_SUNKEN_MIDDLE_GRADIENT_B, ANDROID_BUTTON_SUNKEN_BOTTOM_GRADIENT_B); // Bottom lines DrawAndroidAlternatedHorzLine(ADest, 1, ASize.cx-1, ASize.cy-3, ANDROID_BUTTON_SUNKEN_PREPRELAST_LINE_A, ANDROID_BUTTON_SUNKEN_PREPRELAST_LINE_B); DrawAndroidAlternatedHorzLine(ADest, 2, ASize.cx-2, ASize.cy-2, ANDROID_BUTTON_SUNKEN_PRELAST_LINE_A, ANDROID_BUTTON_SUNKEN_PRELAST_LINE_B); DrawAndroidAlternatedHorzLine(ADest, 3, ASize.cx-3, ASize.cy-1, ANDROID_BUTTON_SUNKEN_LAST_LINE_A, ANDROID_BUTTON_SUNKEN_LAST_LINE_B); end else begin // Top lines DrawAndroidAlternatedHorzLine(ADest, 3, ASize.cx-3, 0, ANDROID_BUTTON_FIRST_LINE_A, ANDROID_BUTTON_FIRST_LINE_B); DrawAndroidAlternatedHorzLine(ADest, 2, ASize.cx-2, 1, ANDROID_BUTTON_SECOND_LINE_A, ANDROID_BUTTON_SECOND_LINE_B); DrawAndroidAlternatedHorzLine(ADest, 1, ASize.cx-1, 2, ANDROID_BUTTON_THIRD_LINE_A, ANDROID_BUTTON_THIRD_LINE_B); // The central gradient lRect := Bounds(0, 3, ASize.cx, (ASize.cy-6) div 3+1); DrawAndroidMixedVertGradientFill(ADest, lRect, ANDROID_BUTTON_TOP_GRADIENT_A, ANDROID_BUTTON_MIDDLE_GRADIENT_A, ANDROID_BUTTON_TOP_GRADIENT_B, ANDROID_BUTTON_MIDDLE_GRADIENT_B); lRect := Bounds(0, 3+(ASize.cy-6) div 3, ASize.cx, (ASize.cy-6) div 3+1); DrawAndroidMixedFill(ADest, lRect, ANDROID_BUTTON_MIDDLE_GRADIENT_A, ANDROID_BUTTON_MIDDLE_GRADIENT_B); lRect := Bounds(0, 3+2*(ASize.cy-6) div 3, ASize.cx, (ASize.cy-6) div 3+1); DrawAndroidMixedVertGradientFill(ADest, lRect, ANDROID_BUTTON_MIDDLE_GRADIENT_A, ANDROID_BUTTON_BOTTOM_GRADIENT_A, ANDROID_BUTTON_MIDDLE_GRADIENT_B, ANDROID_BUTTON_BOTTOM_GRADIENT_B); // Bottom lines DrawAndroidAlternatedHorzLine(ADest, 1, ASize.cx-1, ASize.cy-3, ANDROID_BUTTON_PREPRELAST_LINE_A, ANDROID_BUTTON_PREPRELAST_LINE_B); DrawAndroidAlternatedHorzLine(ADest, 2, ASize.cx-2, ASize.cy-2, ANDROID_BUTTON_PRELAST_LINE_A, ANDROID_BUTTON_PRELAST_LINE_B); DrawAndroidAlternatedHorzLine(ADest, 3, ASize.cx-3, ASize.cy-1, ANDROID_BUTTON_LAST_LINE_A, ANDROID_BUTTON_LAST_LINE_B); end; if csfHasFocus in AState then DrawFocusRect(ADest, Point(5, 5), Size(ASize.cx-10, ASize.cy-10)); // Position calculations ADest.Font.Assign(AStateEx.Font); Str := AStateEx.Caption; lGlyphCaptionHeight := Max(ADest.TextHeight(Str), AStateEx.Glyph.Height); lTextOutPos.X := (ASize.cx - ADest.TextWidth(Str) - AStateEx.Glyph.Width) div 2; lTextOutPos.Y := (ASize.cy - lGlyphCaptionHeight) div 2; lTextOutPos.X := Max(lTextOutPos.X, 5); lTextOutPos.Y := Max(lTextOutPos.Y, 5); // Button glyph if not AStateEx.Glyph.Empty then begin ADest.Draw(lTextOutPos.X, lTextOutPos.Y, AStateEx.Glyph); lGlyphLeftSpacing := AStateEx.Glyph.Width+5; end; // Button text lTextOutPos.X := lTextOutPos.X + lGlyphLeftSpacing; lTextOutPos.Y := (ASize.cy - ADest.TextHeight(Str)) div 2; ADest.Brush.Style := bsClear; ADest.Pen.Style := psSolid; if csfSunken in AState then begin Inc(lTextOutPos.X); Inc(lTextOutPos.Y); end; ADest.TextOut(lTextOutPos.X, lTextOutPos.Y, Str) end; procedure TCDDrawerAndroid.DrawCheckBoxSquare(ADest: TCanvas; ADestPos: TPoint; ASize: TSize; AState: TCDControlState; AStateEx: TCDControlStateEx); begin if csfOn in AState then ADest.Draw(0, 0, bmpCheckboxChecked) else ADest.Draw(0, 0, bmpCheckbox); end; initialization {$I customdrawnimages/android.lrs} RegisterDrawer(TCDDrawerAndroid.Create, dsAndroid); end.